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.Collections; 39import java.util.Enumeration; 40import java.util.HashSet; 41import java.util.Map; 42import java.util.Set; 43import java.util.TreeMap; 44import java.util.zip.ZipEntry; 45import java.util.zip.ZipFile; 46 47import static org.junit.Assert.assertArrayEquals; 48import static org.junit.Assert.assertEquals; 49import static org.junit.Assert.assertFalse; 50import static org.junit.Assert.assertNotNull; 51import static org.junit.Assert.assertTrue; 52 53/** 54 * Unit tests for some methods of {@link AsmGenerator}. 55 */ 56public class AsmGeneratorTest { 57 58 private static final String[] EMPTY_STRING_ARRAY = new String[0]; 59 private MockLog mLog; 60 private ArrayList<String> mOsJarPath; 61 private String mOsDestJar; 62 private File mTempFile; 63 64 // ASM internal name for the the class in java package that should be refactored. 65 private static final String JAVA_CLASS_NAME = "java/lang/JavaClass"; 66 67 @Before 68 public void setUp() throws Exception { 69 mLog = new MockLog(); 70 URL url = this.getClass().getClassLoader().getResource("data/mock_android.jar"); 71 72 mOsJarPath = new ArrayList<>(); 73 //noinspection ConstantConditions 74 mOsJarPath.add(url.getFile()); 75 76 mTempFile = File.createTempFile("mock", ".jar"); 77 mOsDestJar = mTempFile.getAbsolutePath(); 78 mTempFile.deleteOnExit(); 79 } 80 81 @After 82 public void tearDown() throws Exception { 83 if (mTempFile != null) { 84 mTempFile.delete(); 85 mTempFile = null; 86 } 87 } 88 89 @Test 90 public void testClassRenaming() throws IOException, LogAbortException { 91 92 ICreateInfo ci = new ICreateInfo() { 93 @Override 94 public Class<?>[] getInjectedClasses() { 95 // classes to inject in the final JAR 96 return new Class<?>[0]; 97 } 98 99 @Override 100 public String[] getDelegateMethods() { 101 return EMPTY_STRING_ARRAY; 102 } 103 104 @Override 105 public String[] getDelegateClassNatives() { 106 return EMPTY_STRING_ARRAY; 107 } 108 109 @Override 110 public String[] getOverriddenMethods() { 111 // methods to force override 112 return EMPTY_STRING_ARRAY; 113 } 114 115 @Override 116 public String[] getRenamedClasses() { 117 // classes to rename (so that we can replace them) 118 return new String[] { 119 "mock_android.view.View", "mock_android.view._Original_View", 120 "not.an.actual.ClassName", "anoter.fake.NewClassName", 121 }; 122 } 123 124 @Override 125 public String[] getJavaPkgClasses() { 126 return EMPTY_STRING_ARRAY; 127 } 128 129 @Override 130 public Set<String> getExcludedClasses() { 131 return null; 132 } 133 134 @Override 135 public String[] getDeleteReturns() { 136 // methods deleted from their return type. 137 return EMPTY_STRING_ARRAY; 138 } 139 140 @Override 141 public String[] getPromotedFields() { 142 return EMPTY_STRING_ARRAY; 143 } 144 145 @Override 146 public Map<String, InjectMethodRunnable> getInjectedMethodsMap() { 147 return Collections.emptyMap(); 148 } 149 }; 150 151 AsmGenerator agen = new AsmGenerator(mLog, mOsDestJar, ci); 152 153 AsmAnalyzer aa = new AsmAnalyzer(mLog, mOsJarPath, agen, 154 null, // derived from 155 new String[] { // include classes 156 "**" 157 }, 158 Collections.<String>emptySet() /* excluded classes */, 159 new String[]{} /* include files */); 160 aa.analyze(); 161 agen.generate(); 162 163 Set<String> notRenamed = agen.getClassesNotRenamed(); 164 assertArrayEquals(new String[] { "not/an/actual/ClassName" }, notRenamed.toArray()); 165 166 } 167 168 @Test 169 public void testClassRefactoring() throws IOException, LogAbortException { 170 ICreateInfo ci = new ICreateInfo() { 171 @Override 172 public Class<?>[] getInjectedClasses() { 173 // classes to inject in the final JAR 174 return new Class<?>[] { 175 com.android.tools.layoutlib.create.dataclass.JavaClass.class 176 }; 177 } 178 179 @Override 180 public String[] getDelegateMethods() { 181 return EMPTY_STRING_ARRAY; 182 } 183 184 @Override 185 public String[] getDelegateClassNatives() { 186 return EMPTY_STRING_ARRAY; 187 } 188 189 @Override 190 public String[] getOverriddenMethods() { 191 // methods to force override 192 return EMPTY_STRING_ARRAY; 193 } 194 195 @Override 196 public String[] getRenamedClasses() { 197 // classes to rename (so that we can replace them) 198 return EMPTY_STRING_ARRAY; 199 } 200 201 @Override 202 public String[] getJavaPkgClasses() { 203 // classes to refactor (so that we can replace them) 204 return new String[] { 205 "java.lang.JavaClass", "com.android.tools.layoutlib.create.dataclass.JavaClass", 206 }; 207 } 208 209 @Override 210 public Set<String> getExcludedClasses() { 211 return Collections.singleton("java.lang.JavaClass"); 212 } 213 214 @Override 215 public String[] getDeleteReturns() { 216 // methods deleted from their return type. 217 return EMPTY_STRING_ARRAY; 218 } 219 220 @Override 221 public String[] getPromotedFields() { 222 return EMPTY_STRING_ARRAY; 223 } 224 225 @Override 226 public Map<String, InjectMethodRunnable> getInjectedMethodsMap() { 227 return Collections.emptyMap(); 228 } 229 }; 230 231 AsmGenerator agen = new AsmGenerator(mLog, mOsDestJar, ci); 232 233 AsmAnalyzer aa = new AsmAnalyzer(mLog, mOsJarPath, agen, 234 null, // derived from 235 new String[] { // include classes 236 "**" 237 }, 238 Collections.<String>emptySet(), 239 new String[] { /* include files */ 240 "mock_android/data/data*" 241 }); 242 aa.analyze(); 243 agen.generate(); 244 Map<String, ClassReader> output = new TreeMap<>(); 245 Map<String, InputStream> filesFound = new TreeMap<>(); 246 parseZip(mOsDestJar, output, filesFound); 247 boolean injectedClassFound = false; 248 for (ClassReader cr: output.values()) { 249 TestClassVisitor cv = new TestClassVisitor(); 250 cr.accept(cv, 0); 251 injectedClassFound |= cv.mInjectedClassFound; 252 } 253 assertTrue(injectedClassFound); 254 assertArrayEquals(new String[] {"mock_android/data/dataFile"}, 255 filesFound.keySet().toArray()); 256 } 257 258 @Test 259 public void testClassExclusion() throws IOException, LogAbortException { 260 ICreateInfo ci = new ICreateInfo() { 261 @Override 262 public Class<?>[] getInjectedClasses() { 263 return new Class<?>[0]; 264 } 265 266 @Override 267 public String[] getDelegateMethods() { 268 return EMPTY_STRING_ARRAY; 269 } 270 271 @Override 272 public String[] getDelegateClassNatives() { 273 return EMPTY_STRING_ARRAY; 274 } 275 276 @Override 277 public String[] getOverriddenMethods() { 278 // methods to force override 279 return EMPTY_STRING_ARRAY; 280 } 281 282 @Override 283 public String[] getRenamedClasses() { 284 // classes to rename (so that we can replace them) 285 return EMPTY_STRING_ARRAY; 286 } 287 288 @Override 289 public String[] getJavaPkgClasses() { 290 // classes to refactor (so that we can replace them) 291 return EMPTY_STRING_ARRAY; 292 } 293 294 @Override 295 public Set<String> getExcludedClasses() { 296 Set<String> set = new HashSet<>(2); 297 set.add("mock_android.dummy.InnerTest"); 298 set.add("java.lang.JavaClass"); 299 return set; 300 } 301 302 @Override 303 public String[] getDeleteReturns() { 304 // methods deleted from their return type. 305 return EMPTY_STRING_ARRAY; 306 } 307 308 @Override 309 public String[] getPromotedFields() { 310 return EMPTY_STRING_ARRAY; 311 } 312 313 @Override 314 public Map<String, InjectMethodRunnable> getInjectedMethodsMap() { 315 return Collections.emptyMap(); 316 } 317 }; 318 319 AsmGenerator agen = new AsmGenerator(mLog, mOsDestJar, ci); 320 Set<String> excludedClasses = ci.getExcludedClasses(); 321 AsmAnalyzer aa = new AsmAnalyzer(mLog, mOsJarPath, agen, 322 null, // derived from 323 new String[] { // include classes 324 "**" 325 }, 326 excludedClasses, 327 new String[] { /* include files */ 328 "mock_android/data/data*" 329 }); 330 aa.analyze(); 331 agen.generate(); 332 Map<String, ClassReader> output = new TreeMap<>(); 333 Map<String, InputStream> filesFound = new TreeMap<>(); 334 parseZip(mOsDestJar, output, filesFound); 335 for (String s : output.keySet()) { 336 assertFalse(excludedClasses.contains(s)); 337 } 338 assertArrayEquals(new String[] {"mock_android/data/dataFile"}, 339 filesFound.keySet().toArray()); 340 } 341 342 @Test 343 public void testMethodInjection() throws IOException, LogAbortException, 344 ClassNotFoundException, IllegalAccessException, InstantiationException, 345 NoSuchMethodException, InvocationTargetException { 346 ICreateInfo ci = new ICreateInfo() { 347 @Override 348 public Class<?>[] getInjectedClasses() { 349 return new Class<?>[0]; 350 } 351 352 @Override 353 public String[] getDelegateMethods() { 354 return EMPTY_STRING_ARRAY; 355 } 356 357 @Override 358 public String[] getDelegateClassNatives() { 359 return EMPTY_STRING_ARRAY; 360 } 361 362 @Override 363 public String[] getOverriddenMethods() { 364 // methods to force override 365 return EMPTY_STRING_ARRAY; 366 } 367 368 @Override 369 public String[] getRenamedClasses() { 370 // classes to rename (so that we can replace them) 371 return EMPTY_STRING_ARRAY; 372 } 373 374 @Override 375 public String[] getJavaPkgClasses() { 376 // classes to refactor (so that we can replace them) 377 return EMPTY_STRING_ARRAY; 378 } 379 380 @Override 381 public Set<String> getExcludedClasses() { 382 return Collections.emptySet(); 383 } 384 385 @Override 386 public String[] getDeleteReturns() { 387 // methods deleted from their return type. 388 return EMPTY_STRING_ARRAY; 389 } 390 391 @Override 392 public String[] getPromotedFields() { 393 return EMPTY_STRING_ARRAY; 394 } 395 396 @Override 397 public Map<String, InjectMethodRunnable> getInjectedMethodsMap() { 398 return Collections.singletonMap("mock_android.util.EmptyArray", 399 InjectMethodRunnables.CONTEXT_GET_FRAMEWORK_CLASS_LOADER); 400 } 401 }; 402 403 AsmGenerator agen = new AsmGenerator(mLog, mOsDestJar, ci); 404 AsmAnalyzer aa = new AsmAnalyzer(mLog, mOsJarPath, agen, 405 null, // derived from 406 new String[] { // include classes 407 "**" 408 }, 409 ci.getExcludedClasses(), 410 new String[] { /* include files */ 411 "mock_android/data/data*" 412 }); 413 aa.analyze(); 414 agen.generate(); 415 Map<String, ClassReader> output = new TreeMap<>(); 416 Map<String, InputStream> filesFound = new TreeMap<>(); 417 parseZip(mOsDestJar, output, filesFound); 418 final String modifiedClass = "mock_android.util.EmptyArray"; 419 final String modifiedClassPath = modifiedClass.replace('.', '/').concat(".class"); 420 ZipFile zipFile = new ZipFile(mOsDestJar); 421 ZipEntry entry = zipFile.getEntry(modifiedClassPath); 422 assertNotNull(entry); 423 final byte[] bytes; 424 try (InputStream inputStream = zipFile.getInputStream(entry)) { 425 bytes = getByteArray(inputStream); 426 } 427 ClassLoader classLoader = new ClassLoader(getClass().getClassLoader()) { 428 @Override 429 protected Class<?> findClass(String name) throws ClassNotFoundException { 430 if (name.equals(modifiedClass)) { 431 return defineClass(null, bytes, 0, bytes.length); 432 } 433 throw new ClassNotFoundException(name + " not found."); 434 } 435 }; 436 Class<?> emptyArrayClass = classLoader.loadClass(modifiedClass); 437 Object emptyArrayInstance = emptyArrayClass.newInstance(); 438 Method method = emptyArrayClass.getMethod("getFrameworkClassLoader"); 439 Object cl = method.invoke(emptyArrayInstance); 440 assertEquals(classLoader, cl); 441 } 442 443 private static byte[] getByteArray(InputStream stream) throws IOException { 444 ByteArrayOutputStream bos = new ByteArrayOutputStream(); 445 byte[] buffer = new byte[1024]; 446 int read; 447 while ((read = stream.read(buffer, 0, buffer.length)) > -1) { 448 bos.write(buffer, 0, read); 449 } 450 return bos.toByteArray(); 451 } 452 453 private void parseZip(String jarPath, 454 Map<String, ClassReader> classes, 455 Map<String, InputStream> filesFound) throws IOException { 456 457 ZipFile zip = new ZipFile(jarPath); 458 Enumeration<? extends ZipEntry> entries = zip.entries(); 459 ZipEntry entry; 460 while (entries.hasMoreElements()) { 461 entry = entries.nextElement(); 462 if (entry.getName().endsWith(".class")) { 463 ClassReader cr = new ClassReader(zip.getInputStream(entry)); 464 String className = classReaderToClassName(cr); 465 classes.put(className, cr); 466 } else { 467 filesFound.put(entry.getName(), zip.getInputStream(entry)); 468 } 469 } 470 471 } 472 473 private String classReaderToClassName(ClassReader classReader) { 474 if (classReader == null) { 475 return null; 476 } else { 477 return classReader.getClassName().replace('/', '.'); 478 } 479 } 480 481 private class TestClassVisitor extends ClassVisitor { 482 483 boolean mInjectedClassFound = false; 484 485 TestClassVisitor() { 486 super(Main.ASM_VERSION); 487 } 488 489 @Override 490 public void visit(int version, int access, String name, String signature, 491 String superName, String[] interfaces) { 492 assertTrue(!getBase(name).equals(JAVA_CLASS_NAME)); 493 if (name.equals("com/android/tools/layoutlib/create/dataclass/JavaClass")) { 494 mInjectedClassFound = true; 495 } 496 super.visit(version, access, name, signature, superName, interfaces); 497 } 498 499 @Override 500 public FieldVisitor visitField(int access, String name, String desc, 501 String signature, Object value) { 502 assertTrue(testType(Type.getType(desc))); 503 return super.visitField(access, name, desc, signature, value); 504 } 505 506 @SuppressWarnings("hiding") 507 @Override 508 public MethodVisitor visitMethod(int access, String name, String desc, 509 String signature, String[] exceptions) { 510 MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions); 511 return new MethodVisitor(Main.ASM_VERSION, mv) { 512 513 @Override 514 public void visitFieldInsn(int opcode, String owner, String name, 515 String desc) { 516 assertTrue(!getBase(owner).equals(JAVA_CLASS_NAME)); 517 assertTrue(testType(Type.getType(desc))); 518 super.visitFieldInsn(opcode, owner, name, desc); 519 } 520 521 @Override 522 public void visitLdcInsn(Object cst) { 523 if (cst instanceof Type) { 524 assertTrue(testType((Type)cst)); 525 } 526 super.visitLdcInsn(cst); 527 } 528 529 @Override 530 public void visitTypeInsn(int opcode, String type) { 531 assertTrue(!getBase(type).equals(JAVA_CLASS_NAME)); 532 super.visitTypeInsn(opcode, type); 533 } 534 535 @Override 536 public void visitMethodInsn(int opcode, String owner, String name, 537 String desc, boolean itf) { 538 assertTrue(!getBase(owner).equals(JAVA_CLASS_NAME)); 539 assertTrue(testType(Type.getType(desc))); 540 super.visitMethodInsn(opcode, owner, name, desc, itf); 541 } 542 543 }; 544 } 545 546 private boolean testType(Type type) { 547 int sort = type.getSort(); 548 if (sort == Type.OBJECT) { 549 assertTrue(!getBase(type.getInternalName()).equals(JAVA_CLASS_NAME)); 550 } else if (sort == Type.ARRAY) { 551 assertTrue(!getBase(type.getElementType().getInternalName()) 552 .equals(JAVA_CLASS_NAME)); 553 } else if (sort == Type.METHOD) { 554 boolean r = true; 555 for (Type t : type.getArgumentTypes()) { 556 r &= testType(t); 557 } 558 return r & testType(type.getReturnType()); 559 } 560 return true; 561 } 562 563 private String getBase(String className) { 564 if (className == null) { 565 return null; 566 } 567 int pos = className.indexOf('$'); 568 if (pos > 0) { 569 return className.substring(0, pos); 570 } 571 return className; 572 } 573 } 574} 575