/* * Copyright (C) 2008 The Android Open Source Project * * 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. */ import com.sun.javadoc.AnnotationDesc; import com.sun.javadoc.AnnotationTypeDoc; import com.sun.javadoc.AnnotationValue; import com.sun.javadoc.ClassDoc; import com.sun.javadoc.ConstructorDoc; import com.sun.javadoc.Doc; import com.sun.javadoc.ExecutableMemberDoc; import com.sun.javadoc.LanguageVersion; import com.sun.javadoc.MethodDoc; import com.sun.javadoc.PackageDoc; import com.sun.javadoc.Parameter; import com.sun.javadoc.ParameterizedType; import com.sun.javadoc.RootDoc; import com.sun.javadoc.SourcePosition; import com.sun.javadoc.Tag; import com.sun.javadoc.Type; import com.sun.javadoc.TypeVariable; import com.sun.javadoc.AnnotationDesc.ElementValuePair; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; /* */ public class TestCoverageDoclet { public static final int TYPE_FIELD = 0; public static final int TYPE_METHOD = 1; public static final int TYPE_CLASS = 2; public static final int TYPE_PACKAGE = 3; public static final int TYPE_ROOT = 4; public static final int VALUE_RED = 0; public static final int VALUE_YELLOW = 1; public static final int VALUE_GREEN = 2; public static final String[] COLORS = { "#ffa0a0", "#ffffa0", "#a0ffa0" }; public static final String[] TYPES = { "Field", "Method", "Class", "Package", "All packages" }; /** * Holds our basic output directory. */ private File directory; private Map<ExecutableMemberDoc, AnnotationPointer> resolved = new HashMap<ExecutableMemberDoc, AnnotationPointer>(8192); /** * Helper class for comparing element with each other, in oder to determine * an order. Uses lexicographic order of names. */ private class DocComparator implements Comparator<Doc> { public int compare(Doc elem1, Doc elem2) { return elem1.name().compareTo(elem2.name()); } public boolean equals(Doc elem) { return this == elem; } } private class MemberComparator implements Comparator<ExecutableMemberDoc> { public int compare(ExecutableMemberDoc mem1, ExecutableMemberDoc mem2) { return mem1.toString().compareTo(mem2.toString()); } } class MyStats { private String name; private String link; private int elemCnt = 0; private int[] ryg = new int[3]; private String extra; public MyStats(int type, String name, String link) { this.name = name; this.link = link; } public void add(MyStats subStats) { elemCnt++; for (int i = 0; i < ryg.length; i++) { ryg[i]+= subStats.ryg[i]; } } public int getCountFor(int color) { return ryg[color]; } public String getStat() { float coverage = (float)(ryg[1]+ryg[2]) / (float)(ryg[0]+ryg[1]+ryg[2]); return "red: "+ryg[0]+", yellow:"+ryg[1]+", green:"+ryg[2]+",coverage:"+coverage; } public void inc(int color) { ryg[color]++; } public String getLink() { return link; } public String getName() { return name; } public String getExtra() { return extra; } public void setExtra(String extra) { this.extra = extra; } } /** * Holds our comparator instance for everything. */ private DocComparator comparator = new DocComparator(); private MemberComparator membercomparator = new MemberComparator(); /** * Creates a new instance of the TestProgressDoclet for a given target * directory. */ public TestCoverageDoclet(String directory) { this.directory = new File(directory); } /** * Opens a new output file and writes the usual HTML header. Directories * are created on demand. */ private PrintWriter openFile(String name, String title) throws IOException { File file = new File(directory, name); File parent = file.getParentFile(); parent.mkdirs(); PrintWriter printer = new PrintWriter(new FileOutputStream(file)); printer.println("<html>"); printer.println(" <head>"); printer.println(" <title>" + title + "</title>"); printer.println("<style type=\"text/css\">\n"+ "body { }\n"+ "table {border-width: 0px; border: solid; border-collapse: collapse;}\n"+ "table tr td { vertical-align:top; padding:3px; border: 1px solid black;}\n"+ "</style>"); printer.println(" </head>"); printer.println(" <body>"); printer.println(" <h1>" + title + "</h1>"); return printer; } /** * Closes the given output file, writing the usual HTML footer before. */ private void closeFile(PrintWriter printer) { printer.println(" </body>"); printer.println("</html>"); printer.flush(); printer.close(); } private class TablePrinter { private PrintWriter pr; public TablePrinter(PrintWriter pr) { this.pr = pr; } public void printRow(int color, String... columns) { String colo = COLORS[color]; pr.print("<tr style=\"background-color:"+colo+"\">"); for (String col : columns) { pr.print("<td>"+col+"</td>"); } pr.print("</tr>"); } public void printRow(String... columns) { printRow(1, columns); } public void printPlain(String val) { pr.print(val); } } /** * Processes the whole list of classes that JavaDoc knows about. */ private void process(RootDoc root) throws IOException { // 1. traverse all test-classes (those extending JUnit's TestCase) // and collect the annotation info. Print which test classes // need annotating PrintWriter pr = openFile("test-annotation.html", "test class annotation coverage"); TablePrinter printer = new TablePrinter(pr); printer.printPlain("<table>"); printer.printRow("className", "annotated methods", "total methods", "percentage"); ClassDoc[] classes = root.classes(); Arrays.sort(classes, new Comparator<ClassDoc>() { public int compare(ClassDoc c1, ClassDoc c2) { return c1.toString().compareTo(c2.toString()); }}); for (ClassDoc classDoc : classes) { if (extendsJUnitTestCase(classDoc)) { processTestClass(classDoc, printer); } } printer.printPlain("</table>"); closeFile(pr); //dumpInfo(); // 2. traverse all "normal" (non-junit) source files, for each method // get its status and propagate it up the tree MyStats stats = new MyStats(TYPE_ROOT, "All", "aaa.html"); PrintWriter aprinter = openFile("index.html", "All packages"); aprinter.println("Generated " + new Date().toString()); aprinter.println("<br/><a href=\"test-annotation.html\">annotation progress of test classes</a><br/>"); aprinter.println("<br/><a href=\"hidden-doc.html\">hidden classes and methods</a><br/>"); aprinter.println("<br/><a href=\"interfaces.html\">interfaces</a><br/>"); aprinter.println("<h2>Packages</h2>"); aprinter.println("<table>"); PrintWriter hiddenDocPr = openFile("hidden-doc.html", "hidden classes and methods list"); TablePrinter hiddenDocPrinter = new TablePrinter(hiddenDocPr); hiddenDocPrinter.printPlain("<table>"); hiddenDocPrinter.printRow("Package Name", "Class Name", "Method Name"); PrintWriter interfacePr = openFile("interfaces.html", "interface list"); TablePrinter interfacePrinter = new TablePrinter(interfacePr); interfacePrinter.printPlain("<table>"); interfacePrinter.printRow("packageName", "className"); PackageDoc[] packages = root.specifiedPackages(); Arrays.sort(packages, comparator); for (PackageDoc pack : packages) { if (pack.allClasses().length != 0) { if (pack.name().endsWith(".cts")) { // Skip the cts test packages // System.out.println(">>>>>>>>>>>Skip package: " + pack.name()); } else { MyStats subStat = processPackage(pack, hiddenDocPrinter, interfacePrinter); System.out.println("package " + pack.name() + " has " + subStat.getCountFor(0) + " red."); printStats(aprinter, subStat, true); stats.add(subStat); } } } System.out.println("Total has " + stats.getCountFor(0) + " red."); interfacePrinter.printPlain("</table>"); closeFile(interfacePr); hiddenDocPrinter.printPlain("</table>"); closeFile(hiddenDocPr); aprinter.println("</table>"); aprinter.println("<h2>Summary</h2>"); aprinter.println("<table>"); printStats(aprinter, stats, false); aprinter.println("</table>"); closeFile(aprinter); } /*private void processTargetClass(ClassDoc classDoc) { System.out.println("class:"+classDoc); // show all public/protected constructors for (ExecutableMemberDoc constr : classDoc.constructors()) { if (constr.isPublic() || constr.isProtected()) { processTargetMC(constr); } } // show all public/protected methods for (ExecutableMemberDoc method : classDoc.methods()) { if (method.isPublic() || method.isProtected()) { processTargetMC(method); } } }*/ /*private void dumpInfo() { for (Map.Entry<ExecutableMemberDoc, AnnotationPointer> entry : resolved.entrySet()) { ExecutableMemberDoc mdoc = entry.getKey(); AnnotationPointer ap = entry.getValue(); System.out.println("----- entry -----------------------"); System.out.println("target:"+mdoc.toString()); System.out.println("="); for (MethodDoc meth : ap.testMethods) { System.out.println("test method:"+meth); } } }*/ private void processTestClass(ClassDoc classDoc, TablePrinter printer) { // System.out.println("Processing >>> " + classDoc); // collects all testinfo-annotation info of this class ClassDoc targetClass = null; // get the class annotation which names the default test target class AnnotationDesc[] cAnnots = classDoc.annotations(); for (AnnotationDesc cAnnot : cAnnots) { AnnotationTypeDoc atype = cAnnot.annotationType(); if (atype.toString().equals("dalvik.annotation.TestTargetClass")) { // single member annot with one child 'value' ElementValuePair[] cpairs = cAnnot.elementValues(); ElementValuePair evp = cpairs[0]; AnnotationValue av = evp.value(); Object obj = av.value(); // value must be a class doc if (obj instanceof ClassDoc) { targetClass = (ClassDoc) obj; } else if (obj instanceof ParameterizedType) { targetClass = ((ParameterizedType)obj).asClassDoc(); } else throw new RuntimeException("annotation elem value is of type "+obj.getClass().getName()); } } // now visit all methods (junit test methods - therefore we need not visit the constructors AnnotStat ast = new AnnotStat(); //System.out.println("checking:"+classDoc.qualifiedName()); MethodDoc[] methods = classDoc.methods(); String note = ""; if (targetClass == null) { note += "<br/>targetClass annotation missing!<br/>"; } for (MethodDoc methodDoc : methods) { // ignore if it is not a junit test method if (!methodDoc.name().startsWith("test")) continue; if (classDoc.qualifiedName().equals("tests.api.java.io.BufferedInputStreamTest")) { //System.out.println("method: "+methodDoc.toString()); } if (targetClass == null) { // if the targetClass is missing, count all methods as non-annotated ast.incMethodCnt(false); } else { String error = processTestMethod(methodDoc, ast, targetClass); if (error != null) { note+="<br/><b>E:</b> "+error; } } } int man = ast.cntMethodWithAnnot; int mto = ast.cntAllMethods; float perc = mto==0? 100f : ((float)man)/mto * 100f; printer.printRow(man==mto && note.equals("")? 2:0, classDoc.qualifiedName(), ""+ast.cntMethodWithAnnot, ""+ast.cntAllMethods, ""+perc+ note); } private class AnnotStat { int cntMethodWithAnnot = 0; int cntAllMethods = 0; /** * @param correctAnnot */ public void incMethodCnt(boolean correctAnnot) { cntAllMethods++; if (correctAnnot) { cntMethodWithAnnot++; } } } // points from one targetMethod to 0..n testMethods which test the target method private class AnnotationPointer { AnnotationPointer(ExecutableMemberDoc targetMethod) { this.targetMethod = targetMethod; } final ExecutableMemberDoc targetMethod; List<MethodDoc> testMethods = new ArrayList<MethodDoc>(); public void addTestMethod(MethodDoc testMethod) { if (testMethods.contains(testMethod)) { System.out.println("warn: testMethod refers more than once to the targetMethod, testMethod="+testMethod); } else { testMethods.add(testMethod); } } } private String processTestMethod(MethodDoc methodDoc, AnnotStat ast, ClassDoc targetClass) { //System.out.println("processing method: " + methodDoc); // get all per-method-annotation boolean correctAnnot = false; AnnotationDesc[] annots = methodDoc.annotations(); for (AnnotationDesc annot : annots) { if (annot.annotationType().toString().equals("dalvik.annotation.TestInfo")) { ElementValuePair[] pairs = annot.elementValues(); for (ElementValuePair kv : pairs) { if (kv.element().qualifiedName().equals("dalvik.annotation.TestInfo.targets")) { // targets is an [] type AnnotationValue[] targets = (AnnotationValue[]) kv.value().value(); for (AnnotationValue tval : targets) { // the test targets must be annotations themselves AnnotationDesc targetAnnot = (AnnotationDesc) tval.value(); ExecutableMemberDoc targetMethod = getTargetMethod(targetAnnot, targetClass); if (targetMethod != null) { AnnotationPointer tar = getAnnotationPointer(targetMethod, true); tar.addTestMethod(methodDoc); correctAnnot = true; } else { ast.incMethodCnt(false); return "error: could not resolve targetMethod for class "+targetClass+", annotation was:"+targetAnnot+", testMethod = "+methodDoc.toString(); } } } } } // else some other annotation } ast.incMethodCnt(correctAnnot); return null; } private AnnotationPointer getAnnotationPointer(ExecutableMemberDoc targetMethod, boolean create) { AnnotationPointer ap = resolved.get(targetMethod); if (create && ap == null) { ap = new AnnotationPointer(targetMethod); resolved.put(targetMethod, ap); } return ap; } private ExecutableMemberDoc getTargetMethod(AnnotationDesc targetAnnot, ClassDoc targetClass) { // targetAnnot like @android.annotation.TestTarget(methodName="group", methodArgs=int.class) ElementValuePair[] pairs = targetAnnot.elementValues(); String methodName = null; String args = ""; for (ElementValuePair kval : pairs) { if (kval.element().name().equals("methodName")) { methodName = (String) kval.value().value(); } else if (kval.element().name().equals("methodArgs")) { AnnotationValue[] vals = (AnnotationValue[]) kval.value().value(); for (int i = 0; i < vals.length; i++) { AnnotationValue arg = vals[i]; String argV; if (arg.value() instanceof ClassDoc) { ClassDoc cd = (ClassDoc)arg.value(); argV = cd.qualifiedName(); } else { // primitive type or array type // is there a nicer way to do this? argV = arg.toString(); } // strip .class out of args since signature does not contain those if (argV.endsWith(".class")) { argV = argV.substring(0, argV.length()-6); } args+= (i>0? ",":"") + argV; } } } // both methodName and methodArgs != null because of Annotation definition String refSig = methodName+"("+args+")"; //System.out.println("Check " + refSig); // find the matching method in the target class // check all methods for (ExecutableMemberDoc mdoc : targetClass.methods()) { if (equalsSignature(mdoc, refSig)) { return mdoc; } } // check constructors, too for (ExecutableMemberDoc mdoc : targetClass.constructors()) { if (equalsSignature(mdoc, refSig)) { return mdoc; } } return null; } private boolean equalsSignature(ExecutableMemberDoc mdoc, String refSignature) { Parameter[] params = mdoc.parameters(); String targs = ""; for (int i = 0; i < params.length; i++) { Parameter parameter = params[i]; // check for generic type types Type ptype = parameter.type(); TypeVariable typeVar = ptype.asTypeVariable(); String ptname; if (typeVar != null) { ptname = "java.lang.Object"; // the default fallback Type[] bounds = typeVar.bounds(); if (bounds.length > 0) { ClassDoc typeClass = bounds[0].asClassDoc(); ptname = typeClass.qualifiedName(); } } else { // regular var //ptname = parameter.type().qualifiedTypeName(); ptname = parameter.type().toString(); //System.out.println("quali:"+ptname); //ptname = parameter.typeName(); // omit type signature ptname = ptname.replaceAll("<.*>",""); } targs+= (i>0? ",":"") + ptname; } String testSig = mdoc.name()+"("+targs+")"; //return testSig.equals(refSignature); if (testSig.equals(refSignature)) { //System.out.println("found: Sig:"+testSig); return true; } else { //System.out.println("no match: ref = "+refSignature+", test = "+testSig); return false; } } private boolean extendsJUnitTestCase(ClassDoc classDoc) { //junit.framework.TestCase.java ClassDoc curClass = classDoc; while ((curClass = curClass.superclass()) != null) { if (curClass.toString().equals("junit.framework.TestCase")) { return true; } } return false; } /** * Processes the details of a single package. * @param hiddenDocPrinter * @param excludedClassPrinter * @param interfacePrinter */ private MyStats processPackage(PackageDoc pack, TablePrinter hiddenDocPrinter, TablePrinter interfacePrinter) throws IOException { String file = getPackageDir(pack) + "/package.html"; PrintWriter printer = openFile(file, "Package " + pack.name()); MyStats stats = new MyStats(TYPE_PACKAGE, pack.name(), file); printer.println("<table>"); ClassDoc[] classes = pack.allClasses(); Arrays.sort(classes, comparator); for (ClassDoc clazz : classes) { if (extendsJUnitTestCase(clazz)) { printer.println("<tr><td>ignored(junit):"+clazz.name()+"</td></tr>"); } else if (isHiddenClass(clazz)) { hiddenDocPrinter.printRow(pack.name(), clazz.name(), "*"); } else if (clazz.isInterface()) { interfacePrinter.printRow(pack.name(), clazz.name()); } else { MyStats subStats = processClass(clazz, hiddenDocPrinter); printStats(printer, subStats, true); stats.add(subStats); } } printer.println("</table>"); closeFile(printer); return stats; } private boolean isHiddenClass(ClassDoc clazz) { if (clazz == null) { return false; } if (isHiddenDoc(clazz)) { return true; } // If outter class is hidden, this class should be hidden as well return isHiddenClass(clazz.containingClass()); } private boolean isHiddenDoc(Doc doc) { // Since currently we have two kinds of annotations to mark a class as hide: // 1. @hide // 2. {@hide} // So we should consider both conditions. for (Tag t : doc.tags()) { if (t.name().equals("@hide")) { return true; } } for (Tag t : doc.inlineTags()) { if (t.name().equals("@hide")) { return true; } } return false; } private MyStats processClass(ClassDoc clazz, TablePrinter hiddenDocPrinter) throws IOException { //System.out.println("Process source class: " + clazz); String file = getPackageDir(clazz.containingPackage()) + "/" + clazz.name() + ".html"; PrintWriter printer = openFile(file, "Class " + clazz.name()); String packageName = clazz.containingPackage().name(); String className = clazz.name(); MyStats stats = new MyStats(TYPE_CLASS, className, className+".html"); printer.println("<table><tr><td>name</td><td>tested by</td></tr>"); ConstructorDoc[] constructors = clazz.constructors(); Arrays.sort(constructors, comparator); for (ConstructorDoc constructor : constructors) { //System.out.println("constructor: " + constructor); if (isHiddenDoc(constructor)) { hiddenDocPrinter.printRow(packageName, className, constructor.name()); } else if (!isGeneratedConstructor(constructor)) { MyStats subStat = processElement(constructor); printStats(printer, subStat, false); stats.add(subStat); } } MethodDoc[] methods = clazz.methods(); Arrays.sort(methods, comparator); for (MethodDoc method : methods) { //System.out.println("method: " + method); if ("finalize".equals(method.name())) { // Skip finalize method } else if (isHiddenDoc(method)) { hiddenDocPrinter.printRow(packageName, className, method.name()); } else if (method.isAbstract()) { // Skip abstract method } else { MyStats subStat = processElement(method); printStats(printer, subStat, false); stats.add(subStat); } } printer.println("</table>"); closeFile(printer); return stats; } /** * Determines whether a constructor has been automatically generated and is * thus not present in the original source. The only way to find out seems * to compare the source position against the one of the class. If they're * equal, the constructor does not exist. It's a bit hacky, but it works. */ private boolean isGeneratedConstructor(ConstructorDoc doc) { SourcePosition constPos = doc.position(); SourcePosition classPos = doc.containingClass().position(); return ("" + constPos).equals("" + classPos); } /** * Processes a single method/constructor. */ private MyStats processElement(ExecutableMemberDoc method) { //int color = getColor(doc) //derived.add(subStats) AnnotationPointer ap = getAnnotationPointer(method, false); MyStats stats = new MyStats(TYPE_METHOD, "<b>"+method.name() + "</b> "+method.signature(), null); int refCnt = 0; if (ap != null) { refCnt = ap.testMethods.size(); String by = ""; List<MethodDoc> testM = ap.testMethods; Collections.sort(testM, membercomparator); for (MethodDoc teme : testM) { by+= "<br/>"+teme.toString(); } stats.setExtra(by); } // else this class has no single test that targets one of its method if (refCnt == 0) { stats.inc(VALUE_RED); } else if (refCnt == 1) { stats.inc(VALUE_YELLOW); } else { stats.inc(VALUE_GREEN); } return stats; } /** * Prints a single row to a stats table. */ private void printStats(PrintWriter printer, MyStats info, boolean wantLink) { int red = info.getCountFor(VALUE_RED); int yellow = info.getCountFor(VALUE_YELLOW); printer.println("<tr>"); // rule for coloring: // if red > 0 -> red // if yellow > 0 -> yellow // else green int color; if (red > 0) { color = VALUE_RED; } else if (yellow > 0) { color = VALUE_YELLOW; } else { color = VALUE_GREEN; } printer.println("<td bgcolor=\""+COLORS[color]+"\">"); String link = info.getLink(); if (wantLink && link != null) { printer.print("<a href=\"" + link + "\">" + info.getName() + "</a>"); } else { printer.print(info.getName()); } printer.println(" ("+info.getStat()+") </td>"); if (info.getExtra()!=null) { printer.println("<td>"+info.getExtra()+"</td>"); } printer.println("</tr>"); } /** * Returns the directory for a given package. Basically converts embedded * dots in the name into slashes. */ private File getPackageDir(PackageDoc pack) { if (pack == null || pack.name() == null || "".equals(pack.name())) { return new File("."); } else { return new File(pack.name().replace('.', '/')); } } /** * Called by JavaDoc to find our which command line arguments are supported * and how many parameters they take. Part of the JavaDoc API. */ public static int optionLength(String option) { if ("-d".equals(option)) { return 2; } else { return 0; } } /** * Called by JavaDoc to query a specific command line argument. Part of the * JavaDoc API. */ private static String getOption(RootDoc root, String option, int index, String defValue) { String[][] allOptions = root.options(); for (int i = 0; i < allOptions.length; i++) { if (allOptions[i][0].equals(option)) { return allOptions[i][index]; } } return defValue; } /** * Called by JavaDoc to find out which Java version we claim to support. * Part of the JavaDoc API. */ public static LanguageVersion languageVersion() { return LanguageVersion.JAVA_1_5; } /** * The main entry point called by JavaDoc after all required information has * been collected. Part of the JavaDoc API. */ public static boolean start(RootDoc root) { try { String target = getOption(root, "-d", 1, "."); TestCoverageDoclet doclet = new TestCoverageDoclet(target); doclet.process(root); } catch (Exception ex) { ex.printStackTrace(); return false; } return true; } }