view src/ReportGenerator.java @ 14:eec2474fdaaa

* src/ClassInfo.java: Added new helper class. * src/FileUtils.java: Added new method for reading contents of text file. Refactored. * src/ReportGenerator.java: New functionality: methods coverage are printed in package list (previously only class coverage were printed). Refactored. * templates/all_packages_template.html: * templates/index.html: Changed width of left column. * templates/style.css: Tables have black borders (they are more visible). * Makefile: Updated according to previous changes.
author Pavel Tisnovsky <ptisnovs@redhat.com>
date Fri, 23 Mar 2012 11:55:40 +0100
parents 8c9c61f17fd1
children ed99188fff20
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.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;



/**
 * Report generator which process .txt files generated by PrintPublicMethods and
 * PrintTestCoverage classes.
 * 
 * @author Pavel Tisnovsky
 */
public class ReportGenerator
{
    /**
     * Location of internal or external URI to standard API JavaDoc
     */
    private static final String DOC_BASE = "http://docs.oracle.com/javase/6/docs/api";

    /**
     * Read all classes from a file containing its list.
     * 
     * @param allClassListFileName
     * @return
     */
    private static Set<String> readAllClasses(String allClassListFileName)
    {
        // read file content
        List<String> fileContent = FileUtils.readTextFile(allClassListFileName);
        // set of classes should be sorted
        Set<String> allClasses = new TreeSet<String>();
        // add all lines read from a file to a set (and sort them)
        allClasses.addAll(fileContent);
        return allClasses;
    }

    /**
     * Creates file "all_packages.html" which contains link to all checked
     * packages. Structure of this file is based on template stored in
     * "templates/all_packages_template.html".
     * 
     * @param reportDirectory
     *            directory where report is generated
     * @param allClasses
     *            set of all classes
     * @param testedClasses
     *            set of tested classes
     * @param packageNames
     *            set of package names
     * @param classInfoMap 
     */
    private static void printPackageListToFile(String reportDirectory, Set<String> allClasses,
                    Set<String> testedClasses, Set<String> packageNames, Map<String, ClassInfo> classInfoMap)
    {
        List<String> template = FileUtils.readTextFile("templates/all_packages_template.html");
        List<String> out = new LinkedList<String>();
        // iterate through whole template
        for (String templateLine : template)
        {
            // replace text in template where needed
            if ("${PACKAGE_LIST}".equals(templateLine))
            {
                addPackageList(allClasses, testedClasses, packageNames, out, classInfoMap);
            }
            // normal line
            else
            {
                out.add(templateLine);
            }
        }
        // write list of string to a file with given name
        FileUtils.writeTextFile(reportDirectory, "all_packages.html", out);
    }

    /**
     * Create file containing test coverage report for given package. Structure
     * of this file is based on template stored in
     * "templates/all_packages_template.html".
     * 
     * @param reportDirectory
     *            directory where report is generated
     * @param packageName
     *            package for which the report is generated
     * @param testedClasses
     *            set of tested classes
     * @param classInfoMap 
     */
    private static void printReportForPackageToFile(String reportDirectory, String packageName,
                    Set<String> testedClasses, Map<String, ClassInfo> classInfoMap)
    {
        // read HTML template
        List<String> template = FileUtils.readTextFile("templates/package_template.html");
        // list containing all lines of generated HTML file
        List<String> out = new LinkedList<String>();
        // iterate through whole template
        for (String templateLine : template)
        {
            // replace text in template where needed
            if ("${CLASS_LIST}".equals(templateLine))
            {
                addClassList(reportDirectory, packageName, testedClasses, out, classInfoMap);
            }
            else if ("${PACKAGE_NAME}".equals(templateLine))
            {
                out.add("Package " + packageName);
            }
            // normal line
            else
            {
                out.add(templateLine);
            }
        }
        // write list of string to a file with given name
        FileUtils.writeTextFile(reportDirectory, packageName + ".html", out);
    }

    /**
     * Create file containing test coverage report for all classes. Structure of
     * this file is based on template stored in
     * "templates/all_classes_template.html".
     * 
     * @param reportDirectory
     *            directory where report is generated
     * @param usedPackageNames
     *            all checked package names
     * @param testedClasses
     *            set of tested classes
     * @param classInfoMap 
     */
    private static void printReportForAllClassesInOneFile(String reportDirectory, Set<String> usedPackageNames,
                    Set<String> testedClasses, Map<String, ClassInfo> classInfoMap)
    {
        List<String> template = FileUtils.readTextFile("templates/all_classes_template.html");
        List<String> out = new LinkedList<String>();
        // iterate through whole template
        for (String templateLine : template)
        {
            // replace text in template where needed
            if ("${PACKAGE_AND_CLASS_LIST}".equals(templateLine))
            {
                addPackageAndClassList(reportDirectory, usedPackageNames, testedClasses, out, classInfoMap);
            }
            // normal line
            else
            {
                out.add(templateLine);
            }
        }
        // write list of string to a file with given name
        FileUtils.writeTextFile(reportDirectory, "all_classes.html", out);
    }

    /**
     * Add list of all packages to a list of string which represents generated
     * report. Number of all classes and classes covered by tests are also
     * calculated and printed. Background of table rows are changed according
     * to test coverage ratio.
     * 
     * @param allClasses
     *            set of all classes
     * @param testedClasses
     *            set of tested classes
     * @param packageNames
     *            set of package names
     * @param out
     *            list of string which represents generated report
     */
    @SuppressWarnings("boxing")
    private static void addPackageList(Set<String> allClasses, Set<String> testedClasses, Set<String> packageNames,
                    List<String> out, Map<String, ClassInfo> classInfoMap)
    {
        // iterate through all package names
        for (String packageName : packageNames)
        {
            // compute number of all classes in a package
            final int allClassesCnt = numberOfClassesInPackage(packageName, allClasses);
            // compute number of classes covered by tests
            final int testedClassesCnt = numberOfClassesInPackage(packageName, testedClasses);
            // -> in percent
            final float classPercentage = allClassesCnt == 0 ? 0.0f : 100.0f * testedClassesCnt / allClassesCnt;
            // table row background color is based on percentual test coverage ration
            String backgroundColor1 = generateTableRowBackground(classPercentage);

            int allMethodsCnt=0;
            int coveragedMethodsCnt=0;
            for (String className : allClasses)
            {
                // count only classes in given package
                if (className.startsWith(packageName))
                {
                    ClassInfo classInfo = classInfoMap.get(className);
                    allMethodsCnt += classInfo.getAllMethods().size();
                    coveragedMethodsCnt += classInfo.getTestedMethods().size();
                }
            }
            float methodsPercentage = allMethodsCnt == 0 ? 0.0f : 100.0f * coveragedMethodsCnt / allMethodsCnt;
            // table row background color is based on percentual test coverage ration
            String backgroundColor2 = generateTableRowBackground(methodsPercentage);

            // format output string
            String doc = DOC_BASE + "/" + packageName.replace('.', '/') + "/package-summary.html";

            String str = String.format(
                            "<tr><td><a target='ClassesListFrame' href='%s.html'>%s</a></td>" +
                                 "<td style='background-color:%s;text-align:right'>%d</td>" +
                                 "<td style='background-color:%s;text-align:right'>%d</td>" +
                                 "<td style='background-color:%s;text-align:right'>%5.1f %%</td>" +
                                 "<td style='background-color:%s;text-align:right'>%d</td>" +
                                 "<td style='background-color:%s;text-align:right'>%d</td>" +
                                 "<td style='background-color:%s;text-align:right'>%5.1f %%</td>" +
                                 "<td style='text-align:right'><a href='%s' target='_blank'>ext</a></td></tr>",
                            packageName, packageName,
                            backgroundColor1, allClassesCnt, backgroundColor1, testedClassesCnt, backgroundColor1, classPercentage,
                            backgroundColor2, allMethodsCnt, backgroundColor2, coveragedMethodsCnt, backgroundColor2, methodsPercentage,
                            doc);
            out.add(str);
        }
    }

    /**
     * Generate table row background according to test coverage percentual
     * ratio.
     * 
     * @param percentage
     *            test coverage percentual ratio
     * @return string representing HTML color
     */
    private static String generateTableRowBackground(final float percentage)
    {
        String backgroundColor = percentage < 10.0f ? "#ffc0c0" :
                                 percentage == 100.0f ? "#c0ffc0" :
                                 percentage >= 50.0f ? "#ffffc0" :
                                 "#ffffff";
        return backgroundColor;
    }

    /**
     * Calculate number of classes in given package.
     * 
     * @param packageName
     *            name of package
     * @param classes
     *            set of class names
     * @return number of classes in given package
     */
    private static int numberOfClassesInPackage(String packageName, Set<String> classes)
    {
        int cnt = 0;
        // iterate through all class names
        for (String className : classes)
        {
            // count only classes in given package
            if (className.substring(0, className.lastIndexOf('.')).equals(packageName))
            {
                cnt++;
            }
        }
        return cnt;
    }

    /**
     * Add list of all classes to a list of string which represents generated
     * report.
     * 
     * @param packageName
     *            package for which the report is generated
     * @param testedClasses
     *            set of tested classes
     * @param out
     *            list of string which represents generated report
     * @param classInfoMap 
     */
    private static void addClassList(String reportDirectory, String packageName, Set<String> testedClasses, List<String> out, Map<String, ClassInfo> classInfoMap)
    {
        // iterate through all class names
        for (String className : testedClasses)
        {
            // list only classes from given package
            if (className.startsWith(packageName))
            {
                //out.add("<a target='ResultsFrame' href='" + className + ".html'>" + className + "</a><br>");
                out.add(addOneRowToResultsTable(reportDirectory, className, classInfoMap));
            }
        }
    }

    /**
     * Add list of all packages and all its classes to a list of string which
     * represents generated report.
     * 
     * @param reportDirectory
     *            directory where report is generated
     * @param usedPackageNames
     *            all checked package names
     * @param testedClasses
     *            set of tested classes
     * @param out
     *            list of string which represents generated report
     * @param classInfoMap 
     */
    private static void addPackageAndClassList(String reportDirectory, Set<String> usedPackageNames, Set<String> testedClasses, List<String> out, Map<String, ClassInfo> classInfoMap)
    {
        // iterate through all class names
        for (String packageName : usedPackageNames)
        {
            out.add("<h2>Package " + packageName + "</h2>");
            out.add("<table class='classes_list'>");
            out.add("<tr><th>class</th><th>methods</th><th>covered</th><th>ratio</th><th>doc</th></tr>");
            // http://docs.oracle.com/javase/6/docs/api/java/math/BigDecimal.html
            for (String className : testedClasses)
            {
                if (className.startsWith(packageName))
                {
                    out.add(addOneRowToResultsTable(reportDirectory, className, classInfoMap));
                }
            }
            out.add("</table>");
        }
    }

    /**
     * Add one row to a table containing test coverage for given class.
     *
     * @param reportDirectory
     *            directory where report is generated
     * @param className
     *            name of tested class
     */
    private static String addOneRowToResultsTable(String reportDirectory, String className, Map<String, ClassInfo> classInfoMap)
    {
        Set<String> apiMethods = classInfoMap.get(className).getApiMethods();
        Set<String> testedMethods = classInfoMap.get(className).getTestedMethods();
        // compute number of all methods in a class
        final int allMethodsCnt = apiMethods.size();
        // compute number of methods covered by tests
        final int testedMethodsCnt = testedMethods.size();
        // -> in percent
        final float percentage = 100.0f*testedMethodsCnt / allMethodsCnt;

        // construct CSS class for given row based on a code coverage
        final long coverage = Math.round(Math.ceil(percentage / 10.0f) * 10);
        String cssClass = "coverage_" + coverage;

        // table row background color is based on percentual test coverage ration
        String backgroundColor = generateTableRowBackground(percentage);
        String doc = DOC_BASE + "/" + className.replace('.', '/') + ".html";
        // format output string
        String outStr = String.format("<tr class='%s' style='background-color:%s'><td><a target='ResultsFrame' href='%s.html'>%s</a></td><td style='text-align:right'>%d</td><td style='text-align:right'>%d</td><td style='text-align:right'>%5.1f %%</td><td style='text-align:right'><a href='%s' target='_blank'>ext</a></td></tr>",
                cssClass, backgroundColor, className, className,
                Integer.valueOf(allMethodsCnt), Integer.valueOf(testedMethodsCnt), Float.valueOf(percentage), doc);
        return outStr;
    }

    /**
     * Create new HTML file containing report for one tested class.
     *
     * @param reportDirectory
     *            directory where report is generated
     * @param testClass
     *            name of tested class
     * @param allMethods
     *            superset of apiMethods and testedMethods
     * @param apiMethods
     *            methods presented in API
     * @param testedMethods
     *            methods called from tests
     */
    private static void createFileForClass(String reportDirectory, String testClass, Set<String> allMethods,
                    Set<String> apiMethods, Set<String> testedMethods)
    {
        List<String> template = FileUtils.readTextFile("templates/class_template.html");
        List<String> out = new LinkedList<String>();
        // iterate through whole template
        for (String templateLine : template)
        {
            // replace text in template where needed
            if (templateLine.contains("${CLASS_NAME}"))
            {
                out.add(templateLine.replace("${CLASS_NAME}", testClass));
            }
            // replace text in template where needed
            else if (templateLine.contains("${METHOD_LIST}"))
            {
                printReportForAllMethods(allMethods, apiMethods, testedMethods, out);
            }
            // normal line
            else
            {
                out.add(templateLine);
            }
        }
        // write list of string to a file with given name
        FileUtils.writeTextFile(reportDirectory, testClass + ".html", out);
    }

    /**
     * Print report for all methods in given class.
     * 
     * @param allMethods
     *            superset of apiMethods and testedMethods
     * @param apiMethods
     *            methods presented in API
     * @param testedMethods
     *            methods called from tests
     * @param out
     *            list of string which represents generated report
     */
    private static void printReportForAllMethods(Set<String> allMethods, Set<String> apiMethods,
                    Set<String> testedMethods, List<String> out)
    {
        // iterate through all methods
        for (String methodName : allMethods)
        {
            // check if method is coveraged by the test
            boolean coveragedMethod = testedMethods.contains(methodName);
            // when the method is coveraged by the test
            // 'ok' class should be used in generated HTML
            // to allow as to hide such method
            String okClass = coveragedMethod ? " class='coveraged'" : "";
            out.add("<tr" + okClass + "><td>" + constructPrintedMethodName(methodName) + "</td>");
            out.add(printMethodCoverage(methodName, apiMethods));
            out.add(printMethodCoverage(methodName, testedMethods));
            out.add("</tr>\n");
        }
    }

    /**
     * Construct set filled with public API classes.
     * 
     * @param allClasses
     *            set of all API classes (including classes from proprietary
     *            packages)
     * @return set filled with public API classes
     */
    private static Set<String> preparePackageNames(Set<String> allClasses)
    {
        Set<String> packages = new TreeSet<String>();
        // iterate through all class names
        for (String className : allClasses)
        {
            // filter only public API classes
            String packageName = className.substring(0, className.lastIndexOf('.'));
            if (!packageName.startsWith("com.") && !packageName.startsWith("sun"))
            {
                packages.add(packageName);
            }
        }
        return packages;
    }

    private static String constructPrintedMethodName(String methodName)
    {
        String printedMethodName = methodName.replace("<", "&lt;").replace(">", "&gt;");
        int returnTypeEnds = printedMethodName.indexOf(' ');
        int parametersBegin = printedMethodName.indexOf('(');
        String returnType = printedMethodName.substring(0, printedMethodName.indexOf(' '));
        String name = printedMethodName.substring(returnTypeEnds, parametersBegin);
        String params = printedMethodName.substring(parametersBegin);
        
        return String.format("<span style='color:#000080'>%s</span>" + "<span style='color:#008000'>%s</span>"
                        + "<span style='color:#804000'>%s</span>", returnType, name, params);
    }

    private static String printMethodCoverage(String methodName, Set<String> methodSet)
    {
        if (methodSet.contains(methodName))
        {
            return "<td style='background:#80ff80'>present</td>";
        }
        return "<td style='background:#ff8080'>absent</td>";
    }

    private static Set<String> prepareUsedPackageNames(Set<String> allPackageNames, Set<String> testedClasses)
    {
        Set<String> out = new TreeSet<String>();
        for (String packageName : allPackageNames)
        {
            for (String testClass : testedClasses)
            {
                if (testClass.startsWith(packageName))
                {
                    out.add(packageName);
                    break;
                }
            }
        }
        return out;
    }

    /**
     * Create report for all packages. For each package, one package-name.html
     * file is created containing table with basic coverage report.
     * 
     * @param reportDirectory
     *            directory where report is generated
     * @param usedPackageNames
     *            packages for which the report is generated
     * @param testedClasses
     *            set of tested classes
     * @param classInfoMap 
     */
    private static void printReportForAllPackages(String reportDirectory, Set<String> usedPackageNames,
                    Set<String> testedClasses, Map<String, ClassInfo> classInfoMap)
    {
        // iterate through all tested package names
        for (String packageName : usedPackageNames)
        {
            printReportForPackageToFile(reportDirectory, packageName, testedClasses, classInfoMap);
        }
    }

    private static void printReportForAllClasses(String reportDirectory, Set<String> testedClasses, Map<String, ClassInfo> classInfoMap)//String reportDirectory, Set<String> testedClasses)
    {
        // iterate through all tested classes
        for (String testedClass : testedClasses)
        {
            ClassInfo classInfo = classInfoMap.get(testedClass);
            Set<String> apiMethods = classInfo.getApiMethods();
            Set<String> testedMethods = classInfo.getTestedMethods();
            Set<String> allMethods = classInfo.getAllMethods();
            createFileForClass(reportDirectory, testedClass, allMethods, apiMethods, testedMethods);
        }
    }

    private static void printSummaryPage(String reportDirectory, Set<String> allPackageNames, Set<String> allClasses,
                    Set<String> testedClasses, Set<String> usedPackageNames)
    {
        final int numberOfAllPackages = allPackageNames.size();
        final int numberOfAllClasses = allClasses.size();
        final int numberOfUsedPackages = usedPackageNames.size();
        final int numberOfTestedClasses = testedClasses.size();

        List<String> template = FileUtils.readTextFile("templates/summary.html");
        List<String> out = new LinkedList<String>();
        for (String templateLine : template)
        {
            // replace text in template where needed
            if (templateLine.contains("${API_PACKAGES}"))
            {
                out.add(templateLine.replace("${API_PACKAGES}", "" + numberOfAllPackages));
            }
            // replace text in template where needed
            else if (templateLine.contains("${API_CLASSES}"))
            {
                out.add(templateLine.replace("${API_CLASSES}", "" + numberOfAllClasses));
            }
            else
            {
                if (templateLine.contains("${TESTED_PACKAGES}"))
                {
                    out.add(templateLine.replace("${TESTED_PACKAGES}", "" + numberOfUsedPackages));
                }
                // replace text in template where needed
                else if (templateLine.contains("${TESTED_CLASSES}"))
                {
                    out.add(templateLine.replace("${TESTED_CLASSES}", "" + numberOfTestedClasses));
                }
                // replace text in template where needed
                else if (templateLine.contains("${TESTED_PACKAGES_RATIO}"))
                {
                    out.add(templateLine.replace("${TESTED_PACKAGES_RATIO}", ""
                                    + calcRatio(numberOfUsedPackages, numberOfAllPackages)));
                }
                // replace text in template where needed
                else if (templateLine.contains("${TESTED_CLASSES_RATIO}"))
                {
                    out.add(templateLine.replace("${TESTED_CLASSES_RATIO}", ""
                                    + calcRatio(numberOfTestedClasses, numberOfAllClasses)));
                }
                // normal output
                else
                {
                    out.add(templateLine);
                }
            }
        }
        FileUtils.writeTextFile(reportDirectory, "all_results.html", out);
    }

    /**
     * Calculate ratio of two items (usually tested classes vs. all classes) and
     * return textual representation of the calculated ratio (percentage)
     * 
     * @param numberOfTestedItems
     *            number of tested items
     * @param numberOfAllItems
     *            number of all items
     * @return textual representation of the calculated ratio (percentage)
     */
    @SuppressWarnings("boxing")
    private static String calcRatio(int numberOfTestedItems, int numberOfAllItems)
    {
        float ratio = 100.0f * numberOfTestedItems / numberOfAllItems;
        return String.format("%.2f%%", ratio);
    }

    /**
     * Prepare whole report - HTML and txt one.
     *
     * @param allClassListFileName
     * @param testedClassListFileName
     * @param reportDirectory
     */
    private static void prepareReport(String allClassListFileName, String testedClassListFileName,
                    String reportDirectory)
    {
        Set<String> allClasses = readAllClasses(allClassListFileName);
        Set<String> testedClasses = readAllClasses(testedClassListFileName);
        Set<String> allPackageNames = preparePackageNames(allClasses);
        Set<String> usedPackageNames = prepareUsedPackageNames(allPackageNames, testedClasses);

        System.out.println("All class list:    " + allClassListFileName);
        System.out.println("Read " + (allClasses.size()) + " class names");

        System.out.println("Tested class list: " + testedClassListFileName);
        System.out.println("Read " + (testedClasses.size()) + " class names");

        System.out.println("Setting list of " + (allPackageNames.size()) + " all package names");
        System.out.println("Setting list of " + (usedPackageNames.size()) + " used package names");

        System.out.println("Report directory:  " + reportDirectory);

        Map<String, ClassInfo> classInfoMap = new HashMap<String, ClassInfo>();
        for (String className : allClasses)
        {
            classInfoMap.put(className, new ClassInfo(reportDirectory, className));
        }

        printPackageListToFile(reportDirectory, allClasses, testedClasses, usedPackageNames, classInfoMap);
        printReportForAllClassesInOneFile(reportDirectory, usedPackageNames, testedClasses, classInfoMap);
        printReportForAllPackages(reportDirectory, usedPackageNames, testedClasses, classInfoMap);
        printReportForAllClasses(reportDirectory, testedClasses, classInfoMap);
        printSummaryPage(reportDirectory, allPackageNames, allClasses, testedClasses, usedPackageNames);
    }

    /**
     * Entry point to the report generator.
     * 
     * @param args
     *            should contain name of file containing all class list, name of
     *            file containing tested class list and the report directory
     */
    public static void main(String[] args)
    {
        // check if all parameters are specified on command line
        if (args.length != 3)
        {
            System.err.println("Usage allClassListFileName classListFileName reportDirectory");
            System.exit(1);
        }
        // resolve all three parameters
        String allClassListFileName = args[0];
        String testedClassListFileName = args[1];
        String reportDirectory = args[2];
        // and do the report
        prepareReport(allClassListFileName, testedClassListFileName, reportDirectory);
    }
}