view src/PrintTestCoverage.java @ 11:f010a4361c96

2012-02-21 Pavel Tisnovsky <ptisnovs@redhat.com> * src/FileUtils.java: * src/PrintTestCoverage.java: * src/ReportGenerator.java: Fixed (usally minor) issues found by FindBugs.
author Pavel Tisnovsky <ptisnovs@redhat.com>
date Tue, 21 Feb 2012 17:16:21 +0100
parents 342d366654ce
children b6c8372f5723
line wrap: on
line source

/*
  Test coverage tool.

   Copyright (C) 2012 Red Hat

This tool is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2, or (at your option)
any later version.

This tool is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
General Public License for more details.

You should have received a copy of the GNU General Public License
along with this tool; see the file COPYING.  If not, write to the
Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
02110-1301 USA.

Linking this library statically or dynamically with other modules is
making a combined work based on this library.  Thus, the terms and
conditions of the GNU General Public License cover the whole
combination.

As a special exception, the copyright holders of this library give you
permission to link this library with independent modules to produce an
executable, regardless of the license terms of these independent
modules, and to copy and distribute the resulting executable under
terms of your choice, provided that you also meet, for each linked
independent module, the terms and conditions of the license of that
module.  An independent module is a module which is not derived from
or based on this library.  If you modify this library, you may extend
this exception to your version of the library, but you are not
obligated to do so.  If you do not wish to do so, delete this
exception statement from your version.
*/

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;



/**
 * This enumeration represents all record types which could be stored in a
 * constant pool.
 * 
 * @author Pavel Tisnovsky
 */
enum ConstantPoolTag
{
    CONSTANT_Class(7),
    CONSTANT_Fieldref(9),
    CONSTANT_Methodref(10), 
    CONSTANT_InterfaceMethodref(11),
    CONSTANT_String(8),
    CONSTANT_Integer(3),
    CONSTANT_Float(4),
    CONSTANT_Long(5),
    CONSTANT_Double(6),
    CONSTANT_NameAndType(12),
    CONSTANT_Utf8(1);

    /**
     * Value represented one enumeration item.
     */
    private int value;

    /**
     * Constructor
     * 
     * @param value
     *            enumeration value
     */
    private ConstantPoolTag(int value)
    {
        this.value = value;
    }

    /**
     * Getter for a field value.
     * 
     * @return value of a field named value
     */
    public int getValue()
    {
        return this.value;
    }

    /**
     * Converts integer value to a enumeration item.
     *
     * @param value integer value
     * @return selected enumeration item or null
     */
    public static ConstantPoolTag intToConstantPoolTag(int value)
    {
        for (ConstantPoolTag val : values())
        {
            if (val.value == value)
            {
                return val;
            }
        }
        return null;
    }
}

/**
 * Abstract class which represents any constant pool record.
 * 
 * @author Pavel Tisnovsky
 */
abstract class ConstantPoolRecord
{
    /**
     * Tag associated with each constant pool record.
     */
    private ConstantPoolTag tag;

    /**
     * Constructor common for all subclasses.
     * 
     * @param tag
     *            tag associated with each constant pool record.
     */
    public ConstantPoolRecord(ConstantPoolTag tag)
    {
        this.tag = tag;
    }

    /**
     * Get tag associated with each constant pool record.
     * 
     * @return value of the field 'tag'
     */
    public ConstantPoolTag getTag()
    {
        return this.tag;
    }

    /**
     * Set tag associated with each constant pool record.
     * 
     * @param tag
     *            new value of the field 'tag'
     */
    public void setTag(ConstantPoolTag tag)
    {
        this.tag = tag;
    }

    /**
     * Returns true if this record occupies two constant pool entries. Only such
     * entries storing long and double values need to return true.
     * 
     * @return true if this record occupies two constant pool entries
     */
    abstract boolean isRecordOccupyingTwoPoolEntries();

    /**
     * Converts constant pool entry into a printable string.
     * 
     * @param poolEntries
     *            whole constant pool
     * @return constant pool entry represented as a printable string
     */
    abstract public String toString(ConstantPoolRecord[] poolEntries);
}

/**
 * Class representing class record stored in constant pool.
 * 
 * @author Pavel Tisnovsky
 */
class ClassRecord extends ConstantPoolRecord
{
    private int classNameIndex;

    public ClassRecord(ConstantPoolTag tag, int classNameIndex)
    {
        super(tag);
        this.setClassNameIndex(classNameIndex);
    }

    @Override
    boolean isRecordOccupyingTwoPoolEntries()
    {
        return false;
    }

    public void setClassNameIndex(int classNameIndex)
    {
        this.classNameIndex = classNameIndex;
    }

    public int getClassNameIndex()
    {
        return this.classNameIndex;
    }

    @SuppressWarnings("boxing")
    @Override
    public String toString(ConstantPoolRecord[] poolEntries)
    {
        String className = getClassName(poolEntries);
        return String.format("%2d   Class           %3d        %s", this.getTag().getValue(), getClassNameIndex(), className);
    }

    public String getClassName(ConstantPoolRecord[] poolEntries)
    {
        Utf8Record string = (Utf8Record)poolEntries[this.getClassNameIndex() - 1];
        return string.getString();
    }
}

/**
 * Class representing field reference record stored in constant pool.
 * 
 * @author Pavel Tisnovsky
 */
class FieldReferenceRecord extends ConstantPoolRecord
{
    private int classIndex;
    private int nameTypeIndex;

    public FieldReferenceRecord(ConstantPoolTag tag, int classIndex, int nameTypeIndex)
    {
        super(tag);
        this.setClassIndex(classIndex);
        this.setNameTypeIndex(nameTypeIndex);
    }

    @Override
    boolean isRecordOccupyingTwoPoolEntries()
    {
        return false;
    }

    public void setNameTypeIndex(int nameTypeIndex)
    {
        this.nameTypeIndex = nameTypeIndex;
    }

    public int getNameTypeIndex()
    {
        return this.nameTypeIndex;
    }

    public void setClassIndex(int classIndex)
    {
        this.classIndex = classIndex;
    }

    public int getClassIndex()
    {
        return this.classIndex;
    }

    @SuppressWarnings("boxing")
    @Override
    public String toString(ConstantPoolRecord[] poolEntries)
    {
        ClassRecord classRecord = (ClassRecord)poolEntries[this.getClassIndex() - 1];
        NameAndTypeRecord nameAndTypeRecord = (NameAndTypeRecord)poolEntries[this.getNameTypeIndex() -1];
        String className = classRecord.getClassName(poolEntries);
        String name = nameAndTypeRecord.getName(poolEntries);
        String descriptor = nameAndTypeRecord.getDescriptor(poolEntries);

        return String.format("%2d   FieldRef        %3d %3d    %s.%s:%s", getTag().getValue(), getClassIndex(), getNameTypeIndex(), className, name, descriptor);
    }
}

/**
 * Class representing method reference record stored in constant pool.
 * 
 * @author Pavel Tisnovsky
 */
@SuppressWarnings("boxing")
class MethodReferenceRecord extends ConstantPoolRecord
{
    final static Map<Character, String> typeMap = new HashMap<Character, String>();
    static
    {
        typeMap.put('B', "byte");
        typeMap.put('C', "char");
        typeMap.put('S', "short");
        typeMap.put('I', "int");
        typeMap.put('J', "long");
        typeMap.put('F', "float");
        typeMap.put('D', "double");
        typeMap.put('Z', "boolean");
    }

    public int classIndex;
    public int nameTypeIndex;

    public MethodReferenceRecord(ConstantPoolTag tag, int classIndex, int nameTypeIndex)
    {
        super(tag);
        this.classIndex = classIndex;
        this.nameTypeIndex = nameTypeIndex;
    }

    @Override
    boolean isRecordOccupyingTwoPoolEntries()
    {
        return false;
    }

    private int getClassIndex()
    {
        return this.classIndex;
    }

    private int getNameTypeIndex()
    {
        return this.nameTypeIndex;
    }

    public String getPrintableMethodSignature(ConstantPoolRecord[] poolEntries)
    {
        NameAndTypeRecord nameAndTypeRecord = (NameAndTypeRecord)poolEntries[this.getNameTypeIndex() -1];

        String className = getClassName(poolEntries);
        String name = nameAndTypeRecord.getName(poolEntries);
        String descriptor = nameAndTypeRecord.getDescriptor(poolEntries);

        if (name.equals("<init>")) {
            return "<init> " + className + formatParameters(descriptor);
        }
        return formatReturnType(descriptor) + " " + className + "." + name + formatParameters(descriptor);
    }

    private String formatParameters(String descriptor)
    {
        String params = descriptor.substring(1 + descriptor.indexOf('('), descriptor.indexOf(')'));
        StringBuffer out = new StringBuffer();
        int dimensionCount = 0;

        out.append('(');
        int i = 0;
        while (i < params.length())
        {
            char ch = params.charAt(i);
            if (typeMap.containsKey(ch))
            {
                out.append(typeMap.get(ch));
                while (dimensionCount > 0)
                {
                    dimensionCount--;
                    out.append("[]");
                }
            }
            if (ch == 'L')
            {
                i++;
                while (params.charAt(i) != ';')
                {
                    out.append(params.charAt(i) == '/' ? '.' : params.charAt(i));
                    i++;
                }
            }
            if (ch == '[')
            {
                dimensionCount++;
            }
            else
            {
                if (i < params.length() - 1)
                {
                    out.append(',');
                }
            }
            i++;
        }
        out.append(')');
        return out.toString();
    }

    private String formatReturnType(String descriptor)
    {
        String returnType = descriptor.substring(1 + descriptor.indexOf(')'));
        StringBuffer out = new StringBuffer();
        int dimensionCount = 0;
        int i = 0;
        while (i < returnType.length())
        {
            char ch = returnType.charAt(i);
            if (typeMap.containsKey(ch))
            {
                out.append(typeMap.get(ch));
                while (dimensionCount > 0)
                {
                    dimensionCount--;
                    out.append("[]");
                }
            }
            if (ch == '[')
            {
                dimensionCount++;
            }
            if (ch == 'L')
            {
                i++;
                while (returnType.charAt(i) != ';')
                {
                    out.append(returnType.charAt(i) == '/' ? '.' : returnType.charAt(i));
                    i++;
                }
            }
            i++;
        }
        return out.toString();
    }

    public String getClassName(ConstantPoolRecord[] poolEntries)
    {
        ClassRecord classRecord = (ClassRecord)poolEntries[this.getClassIndex() - 1];
        return classRecord.getClassName(poolEntries).replace('/', '.');
    }

    @Override
    public String toString(ConstantPoolRecord[] poolEntries)
    {
        ClassRecord classRecord = (ClassRecord)poolEntries[this.getClassIndex() - 1];
        NameAndTypeRecord nameAndTypeRecord = (NameAndTypeRecord)poolEntries[this.getNameTypeIndex() -1];
        String className = classRecord.getClassName(poolEntries);
        String name = nameAndTypeRecord.getName(poolEntries);
        String descriptor = nameAndTypeRecord.getDescriptor(poolEntries);

        return String.format("%2d   MethodRef       %3d %3d    %s.%s:%s", getTag().getValue(), getClassIndex(), getNameTypeIndex(), className, name, descriptor);
    }
}

/**
 * Class representing interface record stored in constant pool.
 * 
 * @author Pavel Tisnovsky
 */
class InterfaceReferenceRecord extends ConstantPoolRecord
{
    public int classIndex;
    public int nameTypeIndex;

    public InterfaceReferenceRecord(ConstantPoolTag tag, int classIndex, int nameTypeIndex)
    {
        super(tag);
        this.classIndex = classIndex;
        this.nameTypeIndex = nameTypeIndex;
    }

    @Override
    boolean isRecordOccupyingTwoPoolEntries()
    {
        return false;
    }

    private int getClassIndex()
    {
        return this.classIndex;
    }

    private int getNameTypeIndex()
    {
        return this.nameTypeIndex;
    }

    @SuppressWarnings("boxing")
    @Override
    public String toString(ConstantPoolRecord[] poolEntries)
    {
        ClassRecord classRecord = (ClassRecord)poolEntries[this.getClassIndex() - 1];
        NameAndTypeRecord nameAndTypeRecord = (NameAndTypeRecord)poolEntries[this.getNameTypeIndex() -1];
        String className = classRecord.getClassName(poolEntries);
        String name = nameAndTypeRecord.getName(poolEntries);
        String descriptor = nameAndTypeRecord.getDescriptor(poolEntries);

        return String.format("%2d   InterfaceRef    %3d %3d    %s.%s:%s", getTag().getValue(), getClassIndex(), getNameTypeIndex(), className, name, descriptor);
    }
}

/**
 * Class representing name and type record stored in constant pool.
 * 
 * @author Pavel Tisnovsky
 */
class NameAndTypeRecord extends ConstantPoolRecord
{
    public int nameIndex;
    public int descriptorIndex;

    public NameAndTypeRecord(ConstantPoolTag tag, int nameIndex, int descriptorIndex)
    {
        super(tag);
        this.nameIndex = nameIndex;
        this.descriptorIndex = descriptorIndex;
    }

    public String getName(ConstantPoolRecord[] poolEntries)
    {
        Utf8Record classNameRecord = (Utf8Record)poolEntries[this.nameIndex - 1];
        return classNameRecord.getString();
    }

    public String getDescriptor(ConstantPoolRecord[] poolEntries)
    {
        Utf8Record descriptorRecord = (Utf8Record)poolEntries[this.descriptorIndex - 1];
        return descriptorRecord.getString();
    }

    @Override
    boolean isRecordOccupyingTwoPoolEntries()
    {
        return false;
    }

    @SuppressWarnings("boxing")
    @Override
    public String toString(ConstantPoolRecord[] poolEntries)
    {
        String name = getName(poolEntries);
        String descriptor = getDescriptor(poolEntries);
        return String.format("%2d   Name and type   %3d %3d    %s  %s", this.getTag().getValue(), this.nameIndex, this.descriptorIndex, name, descriptor);
    }
}

/**
 * Class representing string record stored in constant pool.
 * 
 * @author Pavel Tisnovsky
 */
class StringRecord extends ConstantPoolRecord
{
    public int stringIndex;

    public StringRecord(ConstantPoolTag tag, int stringIndex)
    {
        super(tag);
        this.stringIndex = stringIndex;
    }

    @Override
    boolean isRecordOccupyingTwoPoolEntries()
    {
        return false;
    }

    private int getStringIndex()
    {
        return this.stringIndex;
    }

    @SuppressWarnings("boxing")
    @Override
    public String toString(ConstantPoolRecord[] poolEntries)
    {
        Utf8Record utf8string = (Utf8Record) (poolEntries[this.getStringIndex() - 1]);
        String string = utf8string.getString();
        return String.format("%2d   Class           %3d        %s", getTag().getValue(), getStringIndex(), string);
    }
}

/**
 * Class representing integer constant record stored in constant pool.
 * 
 * @author Pavel Tisnovsky
 */
class IntegerRecord extends ConstantPoolRecord
{
    public int integerConstant;

    public IntegerRecord(ConstantPoolTag tag, int integerConstant)
    {
        super(tag);
        this.integerConstant = integerConstant;
    }

    @Override
    boolean isRecordOccupyingTwoPoolEntries()
    {
        return false;
    }

    @SuppressWarnings("boxing")
    @Override
    public String toString(ConstantPoolRecord[] poolEntries)
    {
        return String.format("%2d   %-26s %d", getTag().getValue(), "Integer", this.integerConstant);
    }
}

/**
 * Class representing long constant record stored in constant pool.
 * 
 * @author Pavel Tisnovsky
 */
class LongRecord extends ConstantPoolRecord
{
    public long longConstant;

    public LongRecord(ConstantPoolTag tag, int high, int low)
    {
        super(tag);
        this.longConstant = (((long) high) << 32) + low;
    }

    @Override
    boolean isRecordOccupyingTwoPoolEntries()
    {
        return true;
    }

    @SuppressWarnings("boxing")
    @Override
    public String toString(ConstantPoolRecord[] poolEntries)
    {
        return String.format("%2d   %-26s %d", getTag().getValue(), "Long", this.longConstant);
    }
}

/**
 * Class representing float constant record stored in constant pool.
 * 
 * @author Pavel Tisnovsky
 */
class FloatRecord extends ConstantPoolRecord
{
    public float floatConstant;

    public FloatRecord(ConstantPoolTag tag, int bits)
    {
        super(tag);
        this.floatConstant = Float.intBitsToFloat(bits);
    }

    @Override
    boolean isRecordOccupyingTwoPoolEntries()
    {
        return false;
    }

    @SuppressWarnings("boxing")
    @Override
    public String toString(ConstantPoolRecord[] poolEntries)
    {
        return String.format("%2d   %-26s %f", getTag().getValue(), "Float", this.floatConstant);
    }
}

/**
 * Class representing double constant record stored in constant pool.
 * 
 * @author Pavel Tisnovsky
 */
class DoubleRecord extends ConstantPoolRecord
{
    public double doubleConstant;

    public DoubleRecord(ConstantPoolTag tag, int highbits, int lowbits)
    {
        super(tag);
        long val = (((long) highbits) << 32) + lowbits;
        this.doubleConstant = Double.longBitsToDouble(val);
    }

    @Override
    boolean isRecordOccupyingTwoPoolEntries()
    {
        return true;
    }

    @SuppressWarnings("boxing")
    @Override
    public String toString(ConstantPoolRecord[] poolEntries)
    {
        return String.format("%2d   %-26s %f", getTag().getValue(), "Double", this.doubleConstant);
    }
}

/**
 * Class representing UTF-8 string record stored in constant pool.
 * 
 * @author Pavel Tisnovsky
 */
class Utf8Record extends ConstantPoolRecord
{
    public String string;

    public Utf8Record(ConstantPoolTag tag, String string)
    {
        super(tag);
        this.string= string;
    }

    public String getString()
    {
        return this.string;
    }

    @Override
    boolean isRecordOccupyingTwoPoolEntries()
    {
        return false;
    }

    @SuppressWarnings("boxing")
    @Override
    public String toString(ConstantPoolRecord[] poolEntries)
    {
        return String.format("%2d   String                     \"%s\"", getTag().getValue(), this.getString());
    }
}



/**
 * This class is used to find out, which API methods are called from given
 * tests. Bytecode is investigated: this means this tool is based on static
 * coverage checking.
 * 
 * @author Pavel Tisnovsky
 */
public class PrintTestCoverage
{
    /**
     * Default encoding used for all input/output operations.
     */
    private static final String DEFAULT_ENCODING = "UTF-8";

    /**
     * Default name of file containing directory to tests.
     */
    private static final String TEST_DIRECTORY = "test_directory.txt";

    /**
     * Whether to print constant pool to standard output.
     */
    private static final boolean PRINT_CONSTANT_POOL = false;

    @SuppressWarnings("boxing")
    private static void processMagicNumber(FileInputStream fin) throws Exception
    {
        int magic = FileUtils.readFourBytes(fin);
        System.err.format("Magic constant:     0x%x%n", magic);
        if (magic != 0xcafebabe)
        {
            throw new Exception("Improper magic constant");
        }
    }

    @SuppressWarnings("boxing")
    private static void processClassVersion(FileInputStream fin) throws IOException
    {
        int minorVersion = FileUtils.readTwoBytes(fin);
        int majorVersion = FileUtils.readTwoBytes(fin);
        System.err.format("Major version:      %d%n", majorVersion);
        System.err.format("Minor version:      %d%n", minorVersion);
    }

    private static ConstantPoolRecord readConstantPoolEntry(FileInputStream fin) throws IOException
    {
        ConstantPoolTag tag = ConstantPoolTag.intToConstantPoolTag(FileUtils.readOneByte(fin));
        switch (tag)
        {
            case CONSTANT_Class:
            {
                int classNameIndex = FileUtils.readTwoBytes(fin);
                return new ClassRecord(tag, classNameIndex);
            }
            case CONSTANT_Fieldref:
            {
                int classIndex = FileUtils.readTwoBytes(fin);
                int nameTypeIndex = FileUtils.readTwoBytes(fin);
                return new FieldReferenceRecord(tag, classIndex, nameTypeIndex);
            }
            case CONSTANT_InterfaceMethodref:
            {
                int classIndex = FileUtils.readTwoBytes(fin);
                int nameTypeIndex = FileUtils.readTwoBytes(fin);
                return new InterfaceReferenceRecord(tag, classIndex, nameTypeIndex);
            }
            case CONSTANT_Methodref:
            {
                int classIndex = FileUtils.readTwoBytes(fin);
                int nameTypeIndex = FileUtils.readTwoBytes(fin);
                return new MethodReferenceRecord(tag, classIndex, nameTypeIndex);
            }
            case CONSTANT_NameAndType:
            {
                int nameIndex = FileUtils.readTwoBytes(fin);
                int descriptorIndex = FileUtils.readTwoBytes(fin);
                return new NameAndTypeRecord(tag, nameIndex, descriptorIndex);
            }
            case CONSTANT_String:
            {
                int stringIndex = FileUtils.readTwoBytes(fin);
                return new StringRecord(tag, stringIndex);
            }
            case CONSTANT_Integer:
            {
                int constant = FileUtils.readFourBytes(fin);
                return new IntegerRecord(tag, constant);
            }
            case CONSTANT_Long:
            {
                int high = FileUtils.readFourBytes(fin);
                int low = FileUtils.readFourBytes(fin);
                return new LongRecord(tag, high, low);
            }
            case CONSTANT_Float:
            {
                int bits = FileUtils.readFourBytes(fin);
                return new FloatRecord(tag, bits);
            }
            case CONSTANT_Double:
            {
                int highbits = FileUtils.readFourBytes(fin);
                int lowbits = FileUtils.readFourBytes(fin);
                return new DoubleRecord(tag, highbits, lowbits);
            }
            case CONSTANT_Utf8:
            {
                int length = FileUtils.readTwoBytes(fin);
                StringBuffer buf = new StringBuffer(length);
                for (int i = 0; i < length; i++)
                {
                    buf.append((char)FileUtils.readOneByte(fin));
                }
                return new Utf8Record(tag, buf.toString());
            }
            default:
                System.out.println("Unknown tag " + tag);
                return null;
        }
    }

    @SuppressWarnings("boxing")
    private static ConstantPoolRecord[] processContantPool(FileInputStream fin) throws IOException
    {
        ConstantPoolRecord[] poolEntries;

        int constantPoolCount = FileUtils.readTwoBytes(fin) - 1;

        System.err.format("%nConstant pool size: %d%n", constantPoolCount);
        poolEntries = new ConstantPoolRecord[constantPoolCount];

        int i = 0;
        while (i < constantPoolCount)
        {
            ConstantPoolRecord record = readConstantPoolEntry(fin);
            poolEntries[i] = record;
            i += record.isRecordOccupyingTwoPoolEntries() ? 2 : 1;
            //System.err.println(i + "\t" + record.getTag());
        }

        return poolEntries;
    }

    /**
     * Read and returns flags set for the class
     * 
     * @param fin
     *            input stream for the bytecode
     * @param poolEntries
     *            constant pool entries
     * @return flags set for the class
     * @throws IOException
     */
    private static int processClassFlags(FileInputStream fin, ConstantPoolRecord[] poolEntries) throws IOException
    {
        // read flags set for the class
        int flags = FileUtils.readTwoBytes(fin);
        return flags;
    }

    /**
     * @param fin
     * @param poolEntries
     * @return
     * @throws IOException
     */
    private static String readClassOrSuperclassOrInterfaceName(FileInputStream fin, ConstantPoolRecord[] poolEntries)
                    throws IOException
    {
        // read index into constant pool
        int index = FileUtils.readTwoBytes(fin);
        // constant pool array is indexed from zero, but in bytecode, one-based indexing is used
        index--;
        ConstantPoolRecord poolRecord = poolEntries[index];
        if (poolRecord instanceof ClassRecord)
        {
            return ((ClassRecord)poolRecord).getClassName(poolEntries);
        }
        return null;
    }

    private static String processClassName(FileInputStream fin, ConstantPoolRecord[] poolEntries) throws IOException
    {
        return readClassOrSuperclassOrInterfaceName(fin, poolEntries);
    }

    private static String processSuperclassName(FileInputStream fin, ConstantPoolRecord[] poolEntries) throws IOException
    {
        return readClassOrSuperclassOrInterfaceName(fin, poolEntries);
    }

    private static Set<String> processAllImplementedInterfaces(FileInputStream fin, ConstantPoolRecord[] poolEntries) throws IOException
    {
        int interfacesCount = FileUtils.readTwoBytes(fin);
        Set<String> interfaceNames = new TreeSet<String>();
        for (int i = 0; i < interfacesCount; i++)
        {
            interfaceNames.add(readClassOrSuperclassOrInterfaceName(fin, poolEntries));
        }
        return interfaceNames;
    }

    private static void printConstantPool(ConstantPoolRecord[] poolEntries)
    {
        for (ConstantPoolRecord record : poolEntries)
        {
            if (record != null && !(record instanceof Utf8Record))
            {
                System.out.println(record.toString(poolEntries));
            }
        }
    }

    private static void determineAllCalledAPIMethods(String testedClassName, ConstantPoolRecord[] poolEntries, Set<String> methodSet)
    {
        for (ConstantPoolRecord record : poolEntries)
        {
            if (record != null && (record instanceof MethodReferenceRecord))
            {
                MethodReferenceRecord rec = (MethodReferenceRecord) record;
                if (testedClassName.equals(rec.getClassName(poolEntries)))
                {
                    methodSet.add(rec.getPrintableMethodSignature(poolEntries));
                }
            }
        }
    }

    private static void determineTestCoverageForOneTestClass(String testedClassName, File directory, String fileName, Set<String> methodSet)
    {
        FileInputStream fin = null;
        try
        {
            fin = new FileInputStream(new File(directory, fileName));
            try
            {
                processAllRelevantBytecodeParts(testedClassName, methodSet, fin);
            }
            catch (Exception e)
            {
                e.printStackTrace();
            }
            finally
            {
                try
                {
                    fin.close();
                }
                catch (IOException e)
                {
                    e.printStackTrace();
                }
            }
        }
        catch (FileNotFoundException e)
        {
            e.printStackTrace();
        }
    }

    /**
     * Read all relevant parts of bytecode from a .class file.
     * 
     * @param testedClassName
     *            tested class name.
     * @param methodSet
     *            set to which all method should be stored.
     * @param fileInputStream
     *            instance of FileInputStream opened for reading contents of
     *            bytecode.
     * @throws Exception
     *             thrown if bytecode content is broken in any way.
     * @throws IOException
     *             thrown if an I/O error occurs.
     */
    private static void processAllRelevantBytecodeParts(String testedClassName, Set<String> methodSet,
                    FileInputStream fileInputStream) throws Exception, IOException
    {
        // first part of bytecode - magic number
        processMagicNumber(fileInputStream);
        // second part of bytecode - version and subversion
        processClassVersion(fileInputStream);
        // third part of bytecode - constant pool
        ConstantPoolRecord[] poolEntries = processContantPool(fileInputStream);
        // fourth part of bytecode - flags associated to the class
        int flags = processClassFlags(fileInputStream, poolEntries);
        // fifth part of bytecode - class name
        String className = processClassName(fileInputStream, poolEntries);
        // sixth part of bytecode - super class name
        String superClassName = processSuperclassName(fileInputStream, poolEntries);
        // sixth part of bytecode - list of implemented interfaces
        Set<String> interfaceNames = processAllImplementedInterfaces(fileInputStream, poolEntries);
        // process significant parts of bytecode
        printBasicInfoAboutClass(flags, className, superClassName);
        printAllImplementedInterfaces(interfaceNames);
        // this test is needed to process only real tests, not other classes
        boolean isRealTest = interfaceNames.contains("gnu/testlet/Testlet");
        if (isRealTest)
        {
            determineAllCalledAPIMethods(testedClassName, poolEntries, methodSet);
        }
        if (PRINT_CONSTANT_POOL)
        {
            printConstantPool(poolEntries);
        }
        System.err.println();
    }

    /**
     * Print basic informations about the investigated class.
     * 
     * @param flags
     *            flags set for a class read from bytecode.
     * @param className
     *            class name read from bytecode.
     * @param superClassName
     *            super class name read from bytecode.
     */
    private static void printBasicInfoAboutClass(int flags, String className, String superClassName)
    {
        System.err.println("Class flags: " + flags);
        System.err.println("className: " + className);
        System.err.println("superClassName :" + superClassName);
    }

    /**
     * Print list of all interfaces implemented in investigated class.
     *
     * @param interfaceNames set containing interface names.
     */
    private static void printAllImplementedInterfaces(Set<String> interfaceNames)
    {
        System.err.println("Implemented interfaces: " + interfaceNames.size());
        // print name of each implemented interface
        for (String interfaceName : interfaceNames)
        {
            System.err.println("    " + interfaceName);
        }
    }

    /**
     * Print test coverage for selected class.
     * 
     * @param testedClassName
     *            tested class name.
     * @param methodSet
     *            set to which all method should be stored
     * @param list
     *            directory list
     */
    private static void determineTestCoverageForSelectedClass(String testedClassName, Set<String> methodSet, File list)
    {
        // iterate through all files found in given directory
        for (String fileName : list.list())
        {
            // if the file seems to be real bytecode
            if (fileName.endsWith(".class"))
            {
                System.err.println("Checked file: " + fileName);
                // try to process it
                determineTestCoverageForOneTestClass(testedClassName, list, fileName, methodSet);
            }
        }
    }

    /**
     * Print all API methods called from the tests.
     *
     * @param methodSet set filled with API method names
     */
    private static void printAllAPIMethods(Set<String> methodSet)
    {
        // for all method names stored in methodSet
        for (String method : methodSet)
        {
            System.out.println(method);
        }
    }

    /**
     * Print test coverage for given class name.
     * 
     * @param pathToTests
     *            path to directory where test classes are stored
     * @param testedClassName
     *            name of tested class
     */
    private static void printTestCoverage(String pathToTests, String testedClassName)
    {
        Set<String> methodSet = new TreeSet<String>();
        String className = testedClassName.replace('.', '/');
        File list = new File(pathToTests, className);

        // check if directory containing tests for given class name exists
        if (!list.isDirectory())
        {
            /* if does not exists, nothing bad happens */
            System.err.println(list.getAbsolutePath() + " is not a proper directory name");
            return;
        }
        determineTestCoverageForSelectedClass(testedClassName, methodSet, list);
        // we have methodSet filled, let's print it
        printAllAPIMethods(methodSet);
    }

    /**
     * Read path where test classes are stored from configuration file.
     * 
     * @return path to directory where test classes are stored
     * @throws IOException
     *             thrown in case of any I/O errors
     */
    private static String readPathToTest() throws IOException
    {
        // read path where test classes are stored
        BufferedReader fin = null;
        try
        {
            fin = new BufferedReader(new InputStreamReader(new FileInputStream(TEST_DIRECTORY), DEFAULT_ENCODING));
            String directory = fin.readLine();
            return directory;
        }
        catch (FileNotFoundException e)
        {
            return null;
        }
        finally
        {
            // close input stream if it's possible
            if (fin != null)
            {
                fin.close();
            }
        }
    }

    /**
     * Print test coverage for given class name.
     *
     * @param className full class name
     */
    private static void doPrintTestCoverage(String className)
    {
        String pathToTests = null;
        try
        {
            // read path where test classes are stored from configuration file
            pathToTests = readPathToTest();
            // then print test coverage
            printTestCoverage(pathToTests, className);
        }
        catch (IOException e)
        {
            e.printStackTrace();
        }
    }

    /**
     * Entry point to this tool.
     * 
     * @param args
     *            should contain one argument - full name of class to check
     */
    public static void main(String[] args)
    {
        // full name of class to check should be specified on command line
        if (args.length == 1)
        {
            String className = args[0].trim();
            doPrintTestCoverage(className);
        }
        // the name of class to check must be set on command line
        else
        {
            System.err.println("Usage java PrintTestCoverage package.className");
        }
    }

}