Java tutorial
/* Copyright 2008 Google Inc. * * 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.google.devtools.build.wireless.testing.java.injector.coverage; import com.google.devtools.build.wireless.testing.java.injector.InclusionSelector; import org.objectweb.asm.ClassAdapter; import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.Label; import org.objectweb.asm.MethodAdapter; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; import java.util.regex.Pattern; /** * This class adapter injects coverage collection calls at the beginning of each * method invocation. Methods are mapped with an integer which represents the * position of a bit within a bitmask. At runtime the same integer value will * be used to collect the coverage data. * * @author Michele Sama * */ public class CodeCoverageClassAdapter extends ClassAdapter { /** * Defines the pattern of anonymous classes as defined in the JVM * specifications. It looks like "...ClassName$1.methodName(..." */ private final static Pattern ANONYMOUS_CLASS_PATTERN = Pattern.compile("\\$\\d+"); private final CoverageStatisticContainer statisticContainer; /** * Selects which classes to instrument for coverage. */ private final InclusionSelector inclusionSelector = new InclusionSelector(Boolean.FALSE); /** * Specify which level of coverage will be used for the instrumentation. */ private final CoverageMode coverageMode; private String owner; private boolean shouldInstrumentClass = true; private boolean isAutoGeneratedClass = false; private int classOpcode; private String filename; /** * File identifier obtained from the {@link CoverageStatisticContainer}. */ private int fileIndex; /** * Creates an instance which will use a specified Statistic container. * * @param cv the nested ClassVisitor. * @param container The specified container. */ public CodeCoverageClassAdapter(ClassVisitor cv, CoverageStatisticContainer container, String[] coverageInclusion, CoverageMode coverageMode) { super(cv); statisticContainer = container; if (coverageInclusion != null) { inclusionSelector.loadInclusionList(coverageInclusion); } this.coverageMode = coverageMode; } //TODO: make distinction between anonymous and generated classes. private boolean isGeneratedClass(String clazz) { if (clazz == null) { return false; } return ANONYMOUS_CLASS_PATTERN.matcher(clazz).find(); } /** * Selects which class to map for coverage. At runtime mapped methods of * mapped classes will have a position in the bitmask used for method * coverage. * * <p> If the current classname points to an interface it has to be skipped * because interfaces are not marked for coverage. * * <p> If the current class is synthetic it also has to be discarded because * synthetic classes do not correspond to any source class and they would * be a useless overhead. * * @return <code>true</code> if the class has to be mapped for coverage. */ protected boolean isIncluded() { if ((classOpcode & Opcodes.ACC_INTERFACE) > 0 || (classOpcode & Opcodes.ACC_SYNTHETIC) > 0 || isAutoGeneratedClass) { return false; } return shouldInstrumentClass; } /** * Adds the method into the {@link CoverageStatisticContainer} and obtains * the index which will be injected. That index is used to create a new * instance of the method adapter used to inject the coverage behavior. * * <p>Abstract methods must be skipped, because they do not contains code and * the JVM will execute only their overriding implementation. If a method is * abstract {@code opcode} contains {@link Opcodes#ACC_ABSTRACT}. * * @param opcode The type associated with the method. * @param desc the method's descriptor. * @param signature the method's signature. May be null if the method * parameters, return type and exceptions do not use generic types. * @param exceptions The internal names of the method's exception classes. * May be null. * @return The method visitor which must be used to process the method. */ @Override public MethodVisitor visitMethod(int opcode, String name, String desc, String signature, String[] exceptions) { MethodVisitor mv = cv.visitMethod(opcode, name, desc, signature, exceptions); if (isIncluded() && (opcode & Opcodes.ACC_ABSTRACT) == 0) { // TODO: idName should be an integer and not a string. String idName = owner + "." + name + desc; mv = new MethodCoverage(mv, statisticContainer.includeMethod(idName)); } return mv; } /** * Checks if the user requested the given class to be instrumented. */ boolean shouldInstrumentClass(String clazz) { return inclusionSelector.getMostSpecificAction(clazz); } /** * Visits a specific class and pass it to the statistic container. * * @param version The class version. * @param access the class's access flags. * This parameter also indicates if the class is deprecated. * @param name The class' name. * @param signature The signature of this class. May be null if the class is * not a generic one, and does not extend or implement generic classes * or interfaces. * @param superName The name of the super class. * @param interfaces The internal names of the class's interfaces. * May be null. */ @Override public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { owner = name; classOpcode = access; shouldInstrumentClass = shouldInstrumentClass(owner); isAutoGeneratedClass = isGeneratedClass(owner); if (isIncluded()) { statisticContainer.includeClass(name); } cv.visit(version, access, name, signature, superName, interfaces); } /** * Visits a source file name and passes it to the statistic container. * * @param name The source file name. * @param debug Additional debug information to compute the correspondence * between source and compiled elements of the class. May be null. */ @Override public void visitSource(String name, String debug) { filename = owner.substring(0, owner.lastIndexOf('/') + 1) + name.substring(0, name.lastIndexOf(".java")); if (isIncluded()) { fileIndex = statisticContainer.includeFile(filename); } cv.visitSource(name, debug); } /** * For each method of each class which is going to be mapped for coverage this * {@link MethodAdapter} injects code that will record the line has been * executed at runtime. * * <p>If the Method is visited and * <code>coverageMode == CoverageMode.SUMMARY</code> * some code will be injected as shown below: * * <p><pre>public void foo() { * bar(); * }</pre> * into: * <pre>public void foo() { * CoverageManager.setCovered(m); * bar(); * }</pre> * * <p>where <code>m</code> is {@link #methodIndex}, the unique index for * this method. * * <p>If the Method is visited and * <code>coverageMode == CoverageMode.LINE</code> * some code will be injected as shown below: * * <p><pre>public void foo() { * bar(); * bar(); * }</pre> * into: * <pre>public void foo() { * CoverageManager.setLineCovered(c,l); * bar(); * CoverageManager.setLineCovered(c,l); * bar(); * }</pre> * * <p>where <code>c</code> and <code>l</code> are the classId and the lineId * used to map the lines. * * <p> Please note that injected methods or methods from external libraries * which do not provide line numbers will be skipped from line coverage. * * @author Michele Sama */ protected class MethodCoverage extends MethodAdapter { /** * The index to use when flagging the method. This represent the position * in the bitmask which will be used at runtime. */ private int methodIndex = -1; /** * Creates an instance with a fixed index. * * <p>That index is going to be the method position in the bitmask which at * runtime will be used to mark the method as covered. * * @param mv The nested MethodVisitor. * @param index The position of the index in the bitmask. This is a unique * descriptive String which is identifying the method. * * TODO: this should be an incremental integer to minimize memory on * the client. */ public MethodCoverage(MethodVisitor mv, int index) { super(mv); if (filename == null) { throw new IllegalStateException("Filename is null in class: " + owner + ". /nYou are trying to instrument code which was not compiled " + "in debug mode. Please exclude it from the coverage inclusion, " + "or recompile it with debugging enabled."); } methodIndex = index; } /** * Visits each method invocation and as a first instruction inject a call to * flag the method as covered. */ @Override public void visitCode() { if (coverageMode == CoverageMode.SUMMARY) { mv.visitIntInsn(Opcodes.SIPUSH, methodIndex); mv.visitMethodInsn(Opcodes.INVOKESTATIC, CoverageClassNames.COVERAGE_MANAGER, "setCovered", "(I)V"); } mv.visitCode(); /* TODO: increase the count for profiling and start counting the * time but in a different method visitor. */ } /** * Instruments the code by marking as covered the line identified by the * indexes of the current class and of the current line. The following code * is added by the instrumentor: * * <p><code> * CoverageManager.setLineCovered(int fileIndex, int lineIndex) * </code> * * <p>Note that the line is flagged as instrumented when the line number * is visited. If in the compiled code the line number is inserted before * the instructions then the line is flagged as covered also if it throws * an exception (which is the correct behavior). If a compile puts the line * information after the execution then lines throwing exceptions will * not be covered. */ @Override public void visitLineNumber(int value, Label label) { if (coverageMode == CoverageMode.LINE) { // Use the same key for all the classes of the same java file. When we // display the coverage information, the line numbers are relative to // the java file, not the class files. try { int lineIndex = statisticContainer.addInstrumentedLineAndGetLineIndex(filename, value); // Call LineCoverage.setLineCovered(int fileIndex, int lineIndex). mv.visitIntInsn(Opcodes.SIPUSH, fileIndex); mv.visitIntInsn(Opcodes.SIPUSH, lineIndex); mv.visitMethodInsn(Opcodes.INVOKESTATIC, CoverageClassNames.COVERAGE_MANAGER, "setLineCovered", "(II)V"); //lineNumberInClass++; } catch (Exception e) { /* * The line containing a for loop compiled with the OpenJDK 1.6 * appears twice in the bytecode, when the condition is checked and * when the counter is incremented. Code compiled like that raises * this exception for each for loop. */ } } mv.visitLineNumber(value, label); } } }