/* * Copyright (C) 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.builder.testing; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableSet; import com.google.common.io.ByteStreams; import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassWriter; import org.objectweb.asm.Opcodes; import org.objectweb.asm.Type; import org.objectweb.asm.tree.AbstractInsnNode; import org.objectweb.asm.tree.ClassNode; import org.objectweb.asm.tree.FieldNode; import org.objectweb.asm.tree.InnerClassNode; import org.objectweb.asm.tree.InsnList; import org.objectweb.asm.tree.InsnNode; import org.objectweb.asm.tree.LdcInsnNode; import org.objectweb.asm.tree.MethodInsnNode; import org.objectweb.asm.tree.MethodNode; import org.objectweb.asm.tree.TypeInsnNode; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Constructor; import java.util.Collections; import java.util.List; import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.util.jar.JarOutputStream; import java.util.zip.ZipEntry; /** * Given a "standard" android.jar, creates a "mockable" version, where all classes and methods * are not final. Optionally makes all methods return "default" values, instead of throwing the * infamous "Stub!" exceptions. */ public class MockableJarGenerator { private static final int EMPTY_FLAGS = 0; private static final String CONSTRUCTOR = "<init>"; private static final String CLASS_CONSTRUCTOR = "<clinit>"; private static final ImmutableSet<String> ENUM_METHODS = ImmutableSet.of( CLASS_CONSTRUCTOR, "valueOf", "values"); private static final ImmutableSet<Type> INTEGER_LIKE_TYPES = ImmutableSet.of( Type.INT_TYPE, Type.BYTE_TYPE, Type.BOOLEAN_TYPE, Type.CHAR_TYPE, Type.SHORT_TYPE); private final boolean returnDefaultValues; private final ImmutableSet<String> prefixesToSkip = ImmutableSet.of( "java.", "javax.", "org.xml.", "org.w3c.", "junit.", "org.apache.commons.logging"); public MockableJarGenerator(boolean returnDefaultValues) { this.returnDefaultValues = returnDefaultValues; } public void createMockableJar(File input, File output) throws IOException { Preconditions.checkState( output.createNewFile(), "Output file [%s] already exists.", output.getAbsolutePath()); JarFile androidJar = null; JarOutputStream outputStream = null; try { androidJar = new JarFile(input); outputStream = new JarOutputStream(new FileOutputStream(output)); for (JarEntry entry : Collections.list(androidJar.entries())) { InputStream inputStream = androidJar.getInputStream(entry); if (entry.getName().endsWith(".class")) { if (!skipClass(entry.getName().replace("/", "."))) { rewriteClass(entry, inputStream, outputStream); } } else { outputStream.putNextEntry(entry); ByteStreams.copy(inputStream, outputStream); } inputStream.close(); } } finally { if (androidJar != null) { androidJar.close(); } if (outputStream != null) { outputStream.close(); } } } private boolean skipClass(String className) { for (String prefix : prefixesToSkip) { if (className.startsWith(prefix)) { return true; } } return false; } /** * Writes a modified *.class file to the output JAR file. */ private void rewriteClass( JarEntry entry, InputStream inputStream, JarOutputStream outputStream) throws IOException { ClassReader classReader = new ClassReader(inputStream); ClassNode classNode = new ClassNode(Opcodes.ASM5); classReader.accept(classNode, EMPTY_FLAGS); modifyClass(classNode); ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES); classNode.accept(classWriter); outputStream.putNextEntry(new ZipEntry(entry.getName())); outputStream.write(classWriter.toByteArray()); } /** * Modifies a {@link ClassNode} to clear final flags and rewrite byte code. */ @SuppressWarnings("unchecked") private void modifyClass(ClassNode classNode) { // Make the class not final. classNode.access &= ~Opcodes.ACC_FINAL; List<MethodNode> methodNodes = classNode.methods; for (MethodNode methodNode : methodNodes) { methodNode.access &= ~Opcodes.ACC_FINAL; fixMethodBody(methodNode, classNode); } List<FieldNode> fieldNodes = classNode.fields; for (FieldNode fieldNode : fieldNodes) { // Make public instance fields non-final. This is needed e.g. to "mock" SyncResult.stats. if ((fieldNode.access & Opcodes.ACC_PUBLIC) != 0 && (fieldNode.access & Opcodes.ACC_STATIC) == 0) { fieldNode.access &= ~Opcodes.ACC_FINAL; } } List<InnerClassNode> innerClasses = classNode.innerClasses; for (InnerClassNode innerClassNode : innerClasses) { innerClassNode.access &= ~Opcodes.ACC_FINAL; } } /** * Rewrites the method bytecode to remove the "Stub!" exception. */ private void fixMethodBody(MethodNode methodNode, ClassNode classNode) { if ((methodNode.access & Opcodes.ACC_NATIVE) != 0 || (methodNode.access & Opcodes.ACC_ABSTRACT) != 0) { // Abstract and native method don't have bodies to rewrite. return; } if ((classNode.access & Opcodes.ACC_ENUM) != 0 && ENUM_METHODS.contains(methodNode.name)) { // Don't break enum classes. return; } Type returnType = Type.getReturnType(methodNode.desc); InsnList instructions = methodNode.instructions; if (methodNode.name.equals(CONSTRUCTOR)) { // Keep the call to parent constructor, delete the exception after that. boolean deadCode = false; for (AbstractInsnNode instruction : instructions.toArray()) { if (!deadCode) { if (instruction.getOpcode() == Opcodes.INVOKESPECIAL) { instructions.insert(instruction, new InsnNode(Opcodes.RETURN)); // Start removing all following instructions. deadCode = true; } } else { instructions.remove(instruction); } } } else { instructions.clear(); if (returnDefaultValues || methodNode.name.equals(CLASS_CONSTRUCTOR)) { if (INTEGER_LIKE_TYPES.contains(returnType)) { instructions.add(new InsnNode(Opcodes.ICONST_0)); } else if (returnType.equals(Type.LONG_TYPE)) { instructions.add(new InsnNode(Opcodes.LCONST_0)); } else if (returnType.equals(Type.FLOAT_TYPE)) { instructions.add(new InsnNode(Opcodes.FCONST_0)); } else if (returnType.equals(Type.DOUBLE_TYPE)) { instructions.add(new InsnNode(Opcodes.DCONST_0)); } else { instructions.add(new InsnNode(Opcodes.ACONST_NULL)); } instructions.add(new InsnNode(returnType.getOpcode(Opcodes.IRETURN))); } else { instructions.insert(throwExceptionsList(methodNode, classNode)); } } } private static InsnList throwExceptionsList(MethodNode methodNode, ClassNode classNode) { try { String runtimeException = Type.getInternalName(RuntimeException.class); Constructor<RuntimeException> constructor = RuntimeException.class.getConstructor(String.class); InsnList instructions = new InsnList(); instructions.add( new TypeInsnNode(Opcodes.NEW, runtimeException)); instructions.add(new InsnNode(Opcodes.DUP)); String className = classNode.name.replace('/', '.'); instructions.add(new LdcInsnNode("Method " + methodNode.name + " in " + className + " not mocked. " + "See http://g.co/androidstudio/not-mocked for details.")); instructions.add(new MethodInsnNode( Opcodes.INVOKESPECIAL, runtimeException, CONSTRUCTOR, Type.getType(constructor).getDescriptor(), false)); instructions.add(new InsnNode(Opcodes.ATHROW)); return instructions; } catch (NoSuchMethodException e) { throw new RuntimeException(e); } } }