com.google.devtools.build.wireless.testing.java.injector.coverage.CodeCoverageClassAdapter.java Source code

Java tutorial

Introduction

Here is the source code for com.google.devtools.build.wireless.testing.java.injector.coverage.CodeCoverageClassAdapter.java

Source

/* 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);
        }
    }

}