changeset 5:5ef74c026b81

2012-01-17 Pavel Tisnovsky <ptisnovs@redhat.com> * src/FileUtils.java: Added new static methods used by other classes. * src/PrintTestCoverage.java: Added check if the .class file is really Mauve test. More refactoring and Javadoc.
author Pavel Tisnovsky <ptisnovs@redhat.com>
date Tue, 17 Jan 2012 14:02:07 +0100
parents 730c5549c0f9
children 5849d5bfbee0
files ChangeLog src/FileUtils.java src/PrintTestCoverage.java
diffstat 3 files changed, 375 insertions(+), 66 deletions(-) [+]
line wrap: on
line diff
--- a/ChangeLog	Tue Jan 10 14:08:47 2012 +0100
+++ b/ChangeLog	Tue Jan 17 14:02:07 2012 +0100
@@ -1,3 +1,11 @@
+2012-01-17  Pavel Tisnovsky  <ptisnovs@redhat.com>
+
+	* src/FileUtils.java:
+	Added new static methods used by other classes.
+	* src/PrintTestCoverage.java:
+	Added check if the .class file is really Mauve test.
+	More refactoring and Javadoc.
+
 2012-01-10  Pavel Tisnovsky  <ptisnovs@redhat.com>
 
 	* src/FileUtils.java:
--- a/src/FileUtils.java	Tue Jan 10 14:08:47 2012 +0100
+++ b/src/FileUtils.java	Tue Jan 17 14:02:07 2012 +0100
@@ -38,7 +38,9 @@
 
 import java.io.BufferedReader;
 import java.io.BufferedWriter;
+import java.io.EOFException;
 import java.io.File;
+import java.io.FileInputStream;
 import java.io.FileNotFoundException;
 import java.io.FileReader;
 import java.io.FileWriter;
@@ -54,6 +56,11 @@
 class FileUtils
 {
     /**
+     * EOF constant used as special return value in some methods.
+     */
+    private static final int EOF = -1;
+
+    /**
      * Read content of given text file and return it as list of strings. No
      * exception is thrown during reading.
      * 
@@ -222,6 +229,7 @@
      */
     private static void writeAllLinesToTextFile(BufferedWriter bufferedWriter, List<String> lines) throws IOException
     {
+        // for all lines
         for (String line : lines)
         {
             bufferedWriter.write(line);
@@ -230,4 +238,60 @@
         }
     }
 
+    /**
+     * Read one byte from given file input stream with check if EOF is
+     * reached.
+     * 
+     * @param fileInputStream instance of already opened FileInputStream
+     * @return
+     * @throws IOException
+     *             thrown if an I/O error occurs
+     */
+    static int readOneByte(FileInputStream fileInputStream) throws IOException
+    {
+        int i = fileInputStream.read();
+        // -1 means that EOF is reached
+        if (i == EOF)
+        {
+            throw new EOFException("End of file reached!");
+        }
+        return i;
+    }
+
+    /**
+     * Read two bytes from given file input stream with check if EOF is
+     * reached.
+     * 
+     * @param fileInputStream instance of already opened FileInputStream
+     * @return
+     * @throws IOException
+     *             thrown if an I/O error occurs
+     */
+    static int readTwoBytes(FileInputStream fileInputStream) throws IOException
+    {
+        int i1 = readOneByte(fileInputStream);
+        int i2 = readOneByte(fileInputStream);
+        // combine all two read bytes into a word
+        return (i1 << 8) | (i2);
+    }
+
+    /**
+     * Read four bytes from given file input stream with check if EOF is
+     * reached.
+     * 
+     * @param fileInputStream instance of already opened FileInputStream
+     * @return
+     * @throws IOException
+     *             thrown if an I/O error occurs
+     */
+    static int readFourBytes(FileInputStream fileInputStream) throws IOException
+    {
+        int i1 = readOneByte(fileInputStream);
+        int i2 = readOneByte(fileInputStream);
+        int i3 = readOneByte(fileInputStream);
+        int i4 = readOneByte(fileInputStream);
+        // combine all four read bytes into a word
+        return (i1 << 24) | (i2 << 16) | (i3 << 8) | (i4);
+    }
+
 }
--- a/src/PrintTestCoverage.java	Tue Jan 10 14:08:47 2012 +0100
+++ b/src/PrintTestCoverage.java	Tue Jan 17 14:02:07 2012 +0100
@@ -47,6 +47,14 @@
 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),
@@ -61,18 +69,38 @@
     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())
@@ -86,27 +114,65 @@
     }
 }
 
+/**
+ * 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);
 }
 
@@ -599,42 +665,26 @@
     }
 }
 
+
+
 /**
- *
+ * 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
 {
     /**
-     * Name of file containing directory to tests.
+     * Default name of file containing directory to tests.
      */
     private static final String TEST_DIRECTORY = "test_directory.txt";
 
-    private static int readByte(FileInputStream fin) throws IOException
-    {
-        return fin.read();
-    }
-
-    private static int readTwoBytes(FileInputStream fin) throws IOException
-    {
-        int i1 = readByte(fin);
-        int i2 = readByte(fin);
-        return (i1 << 8) | (i2);
-    }
-
-    private static int readFourBytes(FileInputStream fin) throws IOException
-    {
-        int i1 = readByte(fin);
-        int i2 = readByte(fin);
-        int i3 = readByte(fin);
-        int i4 = readByte(fin);
-        return (i1 << 24) | (i2 << 16) | (i3 << 8) | (i4);
-    }
-
     @SuppressWarnings("boxing")
     private static void processMagicNumber(FileInputStream fin) throws Exception
     {
-        int magic = readFourBytes(fin);
+        int magic = FileUtils.readFourBytes(fin);
         System.err.format("Magic constant:     0x%x\n", magic);
         if (magic != 0xcafebabe)
         {
@@ -645,80 +695,80 @@
     @SuppressWarnings("boxing")
     private static void processClassVersion(FileInputStream fin) throws IOException
     {
-        int minorVersion = readTwoBytes(fin);
-        int majorVersion = readTwoBytes(fin);
+        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(readByte(fin));
+        ConstantPoolTag tag = ConstantPoolTag.intToConstantPoolTag(FileUtils.readOneByte(fin));
         switch (tag)
         {
             case CONSTANT_Class:
             {
-                int classNameIndex = readTwoBytes(fin);
+                int classNameIndex = FileUtils.readTwoBytes(fin);
                 return new ClassRecord(tag, classNameIndex);
             }
             case CONSTANT_Fieldref:
             {
-                int classIndex = readTwoBytes(fin);
-                int nameTypeIndex = readTwoBytes(fin);
+                int classIndex = FileUtils.readTwoBytes(fin);
+                int nameTypeIndex = FileUtils.readTwoBytes(fin);
                 return new FieldReferenceRecord(tag, classIndex, nameTypeIndex);
             }
             case CONSTANT_InterfaceMethodref:
             {
-                int classIndex = readTwoBytes(fin);
-                int nameTypeIndex = readTwoBytes(fin);
+                int classIndex = FileUtils.readTwoBytes(fin);
+                int nameTypeIndex = FileUtils.readTwoBytes(fin);
                 return new InterfaceReferenceRecord(tag, classIndex, nameTypeIndex);
             }
             case CONSTANT_Methodref:
             {
-                int classIndex = readTwoBytes(fin);
-                int nameTypeIndex = readTwoBytes(fin);
+                int classIndex = FileUtils.readTwoBytes(fin);
+                int nameTypeIndex = FileUtils.readTwoBytes(fin);
                 return new MethodReferenceRecord(tag, classIndex, nameTypeIndex);
             }
             case CONSTANT_NameAndType:
             {
-                int nameIndex = readTwoBytes(fin);
-                int descriptorIndex = readTwoBytes(fin);
+                int nameIndex = FileUtils.readTwoBytes(fin);
+                int descriptorIndex = FileUtils.readTwoBytes(fin);
                 return new NameAndTypeRecord(tag, nameIndex, descriptorIndex);
             }
             case CONSTANT_String:
             {
-                int stringIndex = readTwoBytes(fin);
+                int stringIndex = FileUtils.readTwoBytes(fin);
                 return new StringRecord(tag, stringIndex);
             }
             case CONSTANT_Integer:
             {
-                int constant = readFourBytes(fin);
+                int constant = FileUtils.readFourBytes(fin);
                 return new IntegerRecord(tag, constant);
             }
             case CONSTANT_Long:
             {
-                int high = readFourBytes(fin);
-                int low = readFourBytes(fin);
+                int high = FileUtils.readFourBytes(fin);
+                int low = FileUtils.readFourBytes(fin);
                 return new LongRecord(tag, high, low);
             }
             case CONSTANT_Float:
             {
-                int bits = readFourBytes(fin);
+                int bits = FileUtils.readFourBytes(fin);
                 return new FloatRecord(tag, bits);
             }
             case CONSTANT_Double:
             {
-                int highbits = readFourBytes(fin);
-                int lowbits = readFourBytes(fin);
+                int highbits = FileUtils.readFourBytes(fin);
+                int lowbits = FileUtils.readFourBytes(fin);
                 return new DoubleRecord(tag, highbits, lowbits);
             }
             case CONSTANT_Utf8:
             {
-                int length = readTwoBytes(fin);
+                int length = FileUtils.readTwoBytes(fin);
                 StringBuffer buf = new StringBuffer(length);
                 for (int i = 0; i < length; i++)
                 {
-                    buf.append((char)readByte(fin));
+                    buf.append((char)FileUtils.readOneByte(fin));
                 }
                 return new Utf8Record(tag, buf.toString());
             }
@@ -733,7 +783,7 @@
     {
         ConstantPoolRecord[] poolEntries = null;
 
-        int constantPoolCount = readTwoBytes(fin) - 1;
+        int constantPoolCount = FileUtils.readTwoBytes(fin) - 1;
 
         System.err.format("\nConstant pool size: %d\n", constantPoolCount);
         poolEntries = new ConstantPoolRecord[constantPoolCount];
@@ -750,6 +800,65 @@
         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;
+    }
+
     @SuppressWarnings("unused")
     private static void printConstantPool(ConstantPoolRecord[] poolEntries)
     {
@@ -762,7 +871,7 @@
         }
     }
 
-    private static void printMethodList(String testedClassName, ConstantPoolRecord[] poolEntries, Set<String> methodSet)
+    private static void determineAllCalledAPIMethods(String testedClassName, ConstantPoolRecord[] poolEntries, Set<String> methodSet)
     {
         for (ConstantPoolRecord record : poolEntries)
         {
@@ -777,7 +886,7 @@
         }
     }
 
-    private static void printTestCoverageForOneTestClass(String testedClassName, File directory, String fileName, Set<String> methodSet)
+    private static void determineTestCoverageForOneTestClass(String testedClassName, File directory, String fileName, Set<String> methodSet)
     {
         FileInputStream fin = null;
         try
@@ -785,11 +894,7 @@
             fin = new FileInputStream(new File(directory, fileName));
             try
             {
-                processMagicNumber(fin);
-                processClassVersion(fin);
-                ConstantPoolRecord[] poolEntries = processContantPool(fin);
-                //printConstantPool(poolEntries);
-                printMethodList(testedClassName, poolEntries, methodSet);
+                processAllRelevantBytecodeParts(testedClassName, methodSet, fin);
             }
             catch (Exception e)
             {
@@ -813,45 +918,176 @@
         }
     }
 
+    /**
+     * 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);
+        }
+        //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 */
+        // 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;
         }
-        for (String fileName : list.list())
-        {
-            if (fileName.endsWith(".class"))
-            {
-                System.err.println("Checked file: " + fileName);
-                printTestCoverageForOneTestClass(testedClassName, list, fileName, methodSet);
-            }
-        }
-        for (String method : methodSet)
-        {
-            System.out.println(method);
-        }
+        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 = new BufferedReader(new FileReader(TEST_DIRECTORY));
         String directory = fin.readLine();
         return directory;
     }
 
+    /**
+     * 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)
@@ -874,6 +1110,7 @@
             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");