Home | History | Annotate | Download | only in utils
      1 /*
      2  * Copyright (C) The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 import java.io.BufferedWriter;
     17 import java.io.File;
     18 import java.io.FileNotFoundException;
     19 import java.io.FileOutputStream;
     20 import java.io.FileWriter;
     21 import java.io.IOException;
     22 import java.util.ArrayList;
     23 import java.util.Collection;
     24 import java.util.Iterator;
     25 
     26 import javax.xml.parsers.DocumentBuilderFactory;
     27 import javax.xml.parsers.ParserConfigurationException;
     28 import javax.xml.transform.Transformer;
     29 import javax.xml.transform.TransformerException;
     30 import javax.xml.transform.TransformerFactory;
     31 import javax.xml.transform.TransformerFactoryConfigurationError;
     32 import javax.xml.transform.dom.DOMSource;
     33 import javax.xml.transform.stream.StreamResult;
     34 
     35 import org.w3c.dom.Attr;
     36 import org.w3c.dom.Document;
     37 import org.w3c.dom.Node;
     38 import org.w3c.dom.NodeList;
     39 
     40 import com.sun.javadoc.AnnotationDesc;
     41 import com.sun.javadoc.AnnotationTypeDoc;
     42 import com.sun.javadoc.AnnotationValue;
     43 import com.sun.javadoc.ClassDoc;
     44 import com.sun.javadoc.Doclet;
     45 import com.sun.javadoc.MethodDoc;
     46 import com.sun.javadoc.RootDoc;
     47 import com.sun.javadoc.AnnotationDesc.ElementValuePair;
     48 
     49 /**
     50  * This is only a very simple and brief JavaDoc parser for the CTS.
     51  *
     52  * Input: The source files of the test cases. It will be represented
     53  *          as a list of ClassDoc
     54  * Output: Generate file description.xml, which defines the TestPackage
     55  *          TestSuite and TestCases.
     56  *
     57  * Note:
     58  *  1. Since this class has dependencies on com.sun.javadoc package which
     59  *       is not implemented on Android. So this class can't be compiled.
     60  *  2. The TestSuite can be embedded, which means:
     61  *      TestPackage := TestSuite*
     62  *      TestSuite := TestSuite* | TestCase*
     63  */
     64 public class DescriptionGenerator extends Doclet {
     65     static final String HOST_CONTROLLER = "dalvik.annotation.HostController";
     66     static final String KNOWN_FAILURE = "dalvik.annotation.KnownFailure";
     67     static final String BROKEN_TEST = "dalvik.annotation.BrokenTest";
     68     static final String SUPPRESSED_TEST = "android.test.suitebuilder.annotation.Suppress";
     69 
     70     static final String JUNIT_TEST_CASE_CLASS_NAME = "junit.framework.testcase";
     71     static final String TAG_PACKAGE = "TestPackage";
     72     static final String TAG_SUITE = "TestSuite";
     73     static final String TAG_CASE = "TestCase";
     74     static final String TAG_TEST = "Test";
     75     static final String TAG_DESCRIPTION = "Description";
     76 
     77     static final String ATTRIBUTE_NAME_VERSION = "version";
     78     static final String ATTRIBUTE_VALUE_VERSION = "1.0";
     79     static final String ATTRIBUTE_NAME_FRAMEWORK = "AndroidFramework";
     80     static final String ATTRIBUTE_VALUE_FRAMEWORK = "Android 1.0";
     81 
     82     static final String ATTRIBUTE_NAME = "name";
     83     static final String ATTRIBUTE_HOST_CONTROLLER = "HostController";
     84 
     85     static final String XML_OUTPUT_PATH = "./description.xml";
     86 
     87     static final String OUTPUT_PATH_OPTION = "-o";
     88 
     89     /**
     90      * Start to parse the classes passed in by javadoc, and generate
     91      * the xml file needed by CTS packer.
     92      *
     93      * @param root The root document passed in by javadoc.
     94      * @return Whether the document has been processed.
     95      */
     96     public static boolean start(RootDoc root) {
     97         ClassDoc[] classes = root.classes();
     98         if (classes == null) {
     99             Log.e("No class found!", null);
    100             return true;
    101         }
    102 
    103         String outputPath = XML_OUTPUT_PATH;
    104         String[][] options = root.options();
    105         for (String[] option : options) {
    106             if (option.length == 2 && option[0].equals(OUTPUT_PATH_OPTION)) {
    107                 outputPath = option[1];
    108             }
    109         }
    110 
    111         XMLGenerator xmlGenerator = null;
    112         try {
    113             xmlGenerator = new XMLGenerator(outputPath);
    114         } catch (ParserConfigurationException e) {
    115             Log.e("Cant initialize XML Generator!", e);
    116             return true;
    117         }
    118 
    119         for (ClassDoc clazz : classes) {
    120             if ((!clazz.isAbstract()) && (isValidJUnitTestCase(clazz))) {
    121                 xmlGenerator.addTestClass(new TestClass(clazz));
    122             }
    123         }
    124 
    125         try {
    126             xmlGenerator.dump();
    127         } catch (Exception e) {
    128             Log.e("Can't dump to XML file!", e);
    129         }
    130 
    131         return true;
    132     }
    133 
    134     /**
    135      * Return the length of any doclet options we recognize
    136      * @param option The option name
    137      * @return The number of words this option takes (including the option) or 0 if the option
    138      * is not recognized.
    139      */
    140     public static int optionLength(String option) {
    141         if (option.equals(OUTPUT_PATH_OPTION)) {
    142             return 2;
    143         }
    144         return 0;
    145     }
    146 
    147     /**
    148      * Check if the class is valid test case inherited from JUnit TestCase.
    149      *
    150      * @param clazz The class to be checked.
    151      * @return If the class is valid test case inherited from JUnit TestCase, return true;
    152      *         else, return false.
    153      */
    154     static boolean isValidJUnitTestCase(ClassDoc clazz) {
    155         while((clazz = clazz.superclass()) != null) {
    156             if (JUNIT_TEST_CASE_CLASS_NAME.equals(clazz.qualifiedName().toLowerCase())) {
    157                 return true;
    158             }
    159         }
    160 
    161         return false;
    162     }
    163 
    164     /**
    165      * Log utility.
    166      */
    167     static class Log {
    168         private static boolean TRACE = true;
    169         private static BufferedWriter mTraceOutput = null;
    170 
    171         /**
    172          * Log the specified message.
    173          *
    174          * @param msg The message to be logged.
    175          */
    176         static void e(String msg, Exception e) {
    177             System.out.println(msg);
    178 
    179             if (e != null) {
    180                 e.printStackTrace();
    181             }
    182         }
    183 
    184         /**
    185          * Add the message to the trace stream.
    186          *
    187          * @param msg The message to be added to the trace stream.
    188          */
    189         public static void t(String msg) {
    190             if (TRACE) {
    191                 try {
    192                     if ((mTraceOutput != null) && (msg != null)) {
    193                         mTraceOutput.write(msg + "\n");
    194                         mTraceOutput.flush();
    195                     }
    196                 } catch (IOException e) {
    197                     e.printStackTrace();
    198                 }
    199             }
    200         }
    201 
    202         /**
    203          * Initialize the trace stream.
    204          *
    205          * @param name The class name.
    206          */
    207         public static void initTrace(String name) {
    208             if (TRACE) {
    209                 try {
    210                     if (mTraceOutput == null) {
    211                         String fileName = "cts_debug_dg_" + name + ".txt";
    212                         mTraceOutput = new BufferedWriter(new FileWriter(fileName));
    213                     }
    214                 } catch (IOException e) {
    215                     e.printStackTrace();
    216                 }
    217             }
    218         }
    219 
    220         /**
    221          * Close the trace stream.
    222          */
    223         public static void closeTrace() {
    224             if (mTraceOutput != null) {
    225                 try {
    226                     mTraceOutput.close();
    227                     mTraceOutput = null;
    228                 } catch (IOException e) {
    229                     e.printStackTrace();
    230                 }
    231             }
    232         }
    233     }
    234 
    235     static class XMLGenerator {
    236         String mOutputPath;
    237 
    238         /**
    239          * This document is used to represent the description XML file.
    240          * It is construct by the classes passed in, which contains the
    241          * information of all the test package, test suite and test cases.
    242          */
    243         Document mDoc;
    244 
    245         XMLGenerator(String outputPath) throws ParserConfigurationException {
    246             mOutputPath = outputPath;
    247 
    248             mDoc = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
    249 
    250             Node testPackageElem = mDoc.appendChild(mDoc.createElement(TAG_PACKAGE));
    251 
    252             setAttribute(testPackageElem, ATTRIBUTE_NAME_VERSION, ATTRIBUTE_VALUE_VERSION);
    253             setAttribute(testPackageElem, ATTRIBUTE_NAME_FRAMEWORK, ATTRIBUTE_VALUE_FRAMEWORK);
    254         }
    255 
    256         void addTestClass(TestClass tc) {
    257             appendSuiteToElement(mDoc.getDocumentElement(), tc);
    258         }
    259 
    260         void dump() throws TransformerFactoryConfigurationError,
    261                 FileNotFoundException, TransformerException {
    262             //rebuildDocument();
    263 
    264             Transformer t = TransformerFactory.newInstance().newTransformer();
    265 
    266             // enable indent in result file
    267             t.setOutputProperty("indent", "yes");
    268             t.setOutputProperty("{http://xml.apache.org/xslt}indent-amount","4");
    269 
    270             File file = new File(mOutputPath);
    271             file.getParentFile().mkdirs();
    272 
    273             t.transform(new DOMSource(mDoc),
    274                     new StreamResult(new FileOutputStream(file)));
    275         }
    276 
    277         /**
    278          * Rebuild the document, merging empty suite nodes.
    279          */
    280         void rebuildDocument() {
    281             // merge empty suite nodes
    282             Collection<Node> suiteElems = getUnmutableChildNodes(mDoc.getDocumentElement());
    283             Iterator<Node> suiteIterator = suiteElems.iterator();
    284             while (suiteIterator.hasNext()) {
    285                 Node suiteElem = suiteIterator.next();
    286 
    287                 mergeEmptySuites(suiteElem);
    288             }
    289         }
    290 
    291         /**
    292          * Merge the test suite which only has one sub-suite. In this case, unify
    293          * the name of the two test suites.
    294          *
    295          * @param suiteElem The suite element of which to be merged.
    296          */
    297         void mergeEmptySuites(Node suiteElem) {
    298             Collection<Node> suiteChildren = getSuiteChildren(suiteElem);
    299             if (suiteChildren.size() > 1) {
    300                 for (Node suiteChild : suiteChildren) {
    301                     mergeEmptySuites(suiteChild);
    302                 }
    303             } else if (suiteChildren.size() == 1) {
    304                 // do merge
    305                 Node child = suiteChildren.iterator().next();
    306 
    307                 // update name
    308                 String newName = getAttribute(suiteElem, ATTRIBUTE_NAME) + "."
    309                         + getAttribute(child, ATTRIBUTE_NAME);
    310                 setAttribute(child, ATTRIBUTE_NAME, newName);
    311 
    312                 // update parent node
    313                 Node parentNode = suiteElem.getParentNode();
    314                 parentNode.removeChild(suiteElem);
    315                 parentNode.appendChild(child);
    316 
    317                 mergeEmptySuites(child);
    318             }
    319         }
    320 
    321         /**
    322          * Get the unmuatable child nodes for specified node.
    323          *
    324          * @param node The specified node.
    325          * @return A collection of copied child node.
    326          */
    327         private Collection<Node> getUnmutableChildNodes(Node node) {
    328             ArrayList<Node> nodes = new ArrayList<Node>();
    329             NodeList nodelist = node.getChildNodes();
    330 
    331             for (int i = 0; i < nodelist.getLength(); i++) {
    332                 nodes.add(nodelist.item(i));
    333             }
    334 
    335             return nodes;
    336         }
    337 
    338         /**
    339          * Append a named test suite to a specified element. Including match with
    340          * the existing suite nodes and do the real creation and append.
    341          *
    342          * @param elem The specified element.
    343          * @param testSuite The test suite to be appended.
    344          */
    345         void appendSuiteToElement(Node elem, TestClass testSuite) {
    346             String suiteName = testSuite.mName;
    347             Collection<Node> children = getSuiteChildren(elem);
    348             int dotIndex = suiteName.indexOf('.');
    349             String name = dotIndex == -1 ? suiteName : suiteName.substring(0, dotIndex);
    350 
    351             boolean foundMatch = false;
    352             for (Node child : children) {
    353                 String childName = child.getAttributes().getNamedItem(ATTRIBUTE_NAME)
    354                         .getNodeValue();
    355 
    356                 if (childName.equals(name)) {
    357                     foundMatch = true;
    358                     if (dotIndex == -1) {
    359                         appendTestCases(child, testSuite.mCases);
    360                     } else {
    361                         testSuite.mName = suiteName.substring(dotIndex + 1, suiteName.length());
    362                         appendSuiteToElement(child, testSuite);
    363                     }
    364                 }
    365 
    366             }
    367 
    368             if (!foundMatch) {
    369                 appendSuiteToElementImpl(elem, testSuite);
    370             }
    371         }
    372 
    373         /**
    374          * Get the test suite child nodes of a specified element.
    375          *
    376          * @param elem The specified element node.
    377          * @return The matched child nodes.
    378          */
    379         Collection<Node> getSuiteChildren(Node elem) {
    380             ArrayList<Node> suites = new ArrayList<Node>();
    381 
    382             NodeList children = elem.getChildNodes();
    383             for (int i = 0; i < children.getLength(); i++) {
    384                 Node child = children.item(i);
    385 
    386                 if (child.getNodeName().equals(DescriptionGenerator.TAG_SUITE)) {
    387                     suites.add(child);
    388                 }
    389             }
    390 
    391             return suites;
    392         }
    393 
    394         /**
    395          * Create test case node according to the given method names, and append them
    396          * to the test suite element.
    397          *
    398          * @param elem The test suite element.
    399          * @param cases A collection of test cases included by the test suite class.
    400          */
    401         void appendTestCases(Node elem, Collection<TestMethod> cases) {
    402             if (cases.isEmpty()) {
    403                 // if no method, remove from parent
    404                 elem.getParentNode().removeChild(elem);
    405             } else {
    406                 for (TestMethod caze : cases) {
    407                     if (caze.mIsBroken || caze.mIsSuppressed || caze.mKnownFailure != null) {
    408                         continue;
    409                     }
    410                     Node caseNode = elem.appendChild(mDoc.createElement(TAG_TEST));
    411 
    412                     setAttribute(caseNode, ATTRIBUTE_NAME, caze.mName);
    413                     if ((caze.mController != null) && (caze.mController.length() != 0)) {
    414                         setAttribute(caseNode, ATTRIBUTE_HOST_CONTROLLER, caze.mController);
    415                     }
    416 
    417                     if (caze.mDescription != null && !caze.mDescription.equals("")) {
    418                         caseNode.appendChild(mDoc.createElement(TAG_DESCRIPTION))
    419                                 .setTextContent(caze.mDescription);
    420                     }
    421                 }
    422             }
    423         }
    424 
    425         /**
    426          * Set the attribute of element.
    427          *
    428          * @param elem The element to be set attribute.
    429          * @param name The attribute name.
    430          * @param value The attribute value.
    431          */
    432         protected void setAttribute(Node elem, String name, String value) {
    433             Attr attr = mDoc.createAttribute(name);
    434             attr.setNodeValue(value);
    435 
    436             elem.getAttributes().setNamedItem(attr);
    437         }
    438 
    439         /**
    440          * Get the value of a specified attribute of an element.
    441          *
    442          * @param elem The element node.
    443          * @param name The attribute name.
    444          * @return The value of the specified attribute.
    445          */
    446         private String getAttribute(Node elem, String name) {
    447             return elem.getAttributes().getNamedItem(name).getNodeValue();
    448         }
    449 
    450         /**
    451          * Do the append, including creating test suite nodes and test case nodes, and
    452          * append them to the element.
    453          *
    454          * @param elem The specified element node.
    455          * @param testSuite The test suite to be append.
    456          */
    457         void appendSuiteToElementImpl(Node elem, TestClass testSuite) {
    458             Node parent = elem;
    459             String suiteName = testSuite.mName;
    460 
    461             int dotIndex;
    462             while ((dotIndex = suiteName.indexOf('.')) != -1) {
    463                 String name = suiteName.substring(0, dotIndex);
    464 
    465                 Node suiteElem = parent.appendChild(mDoc.createElement(TAG_SUITE));
    466                 setAttribute(suiteElem, ATTRIBUTE_NAME, name);
    467 
    468                 parent = suiteElem;
    469                 suiteName = suiteName.substring(dotIndex + 1, suiteName.length());
    470             }
    471 
    472             Node leafSuiteElem = parent.appendChild(mDoc.createElement(TAG_CASE));
    473             setAttribute(leafSuiteElem, ATTRIBUTE_NAME, suiteName);
    474 
    475             appendTestCases(leafSuiteElem, testSuite.mCases);
    476         }
    477     }
    478 
    479     /**
    480      * Represent the test class.
    481      */
    482     static class TestClass {
    483         String mName;
    484         Collection<TestMethod> mCases;
    485 
    486         /**
    487          * Construct an test suite object.
    488          *
    489          * @param name Full name of the test suite, such as "com.google.android.Foo"
    490          * @param cases The test cases included in this test suite.
    491          */
    492         TestClass(String name, Collection<TestMethod> cases) {
    493             mName = name;
    494             mCases = cases;
    495         }
    496 
    497         /**
    498          * Construct a TestClass object using ClassDoc.
    499          *
    500          * @param clazz The specified ClassDoc.
    501          */
    502         TestClass(ClassDoc clazz) {
    503             mName = clazz.toString();
    504             mCases = getTestMethods(clazz);
    505         }
    506 
    507         /**
    508          * Get all the TestMethod from a ClassDoc, including inherited methods.
    509          *
    510          * @param clazz The specified ClassDoc.
    511          * @return A collection of TestMethod.
    512          */
    513         Collection<TestMethod> getTestMethods(ClassDoc clazz) {
    514             Collection<MethodDoc> methods = getAllMethods(clazz);
    515 
    516             ArrayList<TestMethod> cases = new ArrayList<TestMethod>();
    517             Iterator<MethodDoc> iterator = methods.iterator();
    518 
    519             while (iterator.hasNext()) {
    520                 MethodDoc method = iterator.next();
    521 
    522                 String name = method.name();
    523 
    524                 AnnotationDesc[] annotations = method.annotations();
    525                 String controller = "";
    526                 String knownFailure = null;
    527                 boolean isBroken = false;
    528                 boolean isSuppressed = false;
    529                 for (AnnotationDesc cAnnot : annotations) {
    530 
    531                     AnnotationTypeDoc atype = cAnnot.annotationType();
    532                     if (atype.toString().equals(HOST_CONTROLLER)) {
    533                         controller = getAnnotationDescription(cAnnot);
    534                     } else if (atype.toString().equals(KNOWN_FAILURE)) {
    535                         knownFailure = getAnnotationDescription(cAnnot);
    536                     } else if (atype.toString().equals(BROKEN_TEST)) {
    537                         isBroken = true;
    538                     } else if (atype.toString().equals(SUPPRESSED_TEST)) {
    539                         isSuppressed = true;
    540                     }
    541                 }
    542 
    543                 if (name.startsWith("test")) {
    544                     cases.add(new TestMethod(name, method.commentText(), controller, knownFailure,
    545                             isBroken, isSuppressed));
    546                 }
    547             }
    548 
    549             return cases;
    550         }
    551 
    552         /**
    553          * Get annotation description.
    554          *
    555          * @param cAnnot The annotation.
    556          */
    557         String getAnnotationDescription(AnnotationDesc cAnnot) {
    558             ElementValuePair[] cpairs = cAnnot.elementValues();
    559             ElementValuePair evp = cpairs[0];
    560             AnnotationValue av = evp.value();
    561             String description = av.toString();
    562             // FIXME: need to find out the reason why there are leading and trailing "
    563             description = description.substring(1, description.length() -1);
    564             return description;
    565         }
    566 
    567         /**
    568          * Get all MethodDoc of a ClassDoc, including inherited methods.
    569          *
    570          * @param clazz The specified ClassDoc.
    571          * @return A collection of MethodDoc.
    572          */
    573         Collection<MethodDoc> getAllMethods(ClassDoc clazz) {
    574             ArrayList<MethodDoc> methods = new ArrayList<MethodDoc>();
    575 
    576             for (MethodDoc method : clazz.methods()) {
    577                 methods.add(method);
    578             }
    579 
    580             ClassDoc superClass = clazz.superclass();
    581             while (superClass != null) {
    582                 for (MethodDoc method : superClass.methods()) {
    583                     methods.add(method);
    584                 }
    585 
    586                 superClass = superClass.superclass();
    587             }
    588 
    589             return methods;
    590         }
    591 
    592     }
    593 
    594     /**
    595      * Represent the test method inside the test class.
    596      */
    597     static class TestMethod {
    598         String mName;
    599         String mDescription;
    600         String mController;
    601         String mKnownFailure;
    602         boolean mIsBroken;
    603         boolean mIsSuppressed;
    604 
    605         /**
    606          * Construct an test case object.
    607          *
    608          * @param name The name of the test case.
    609          * @param description The description of the test case.
    610          * @param knownFailure The reason of known failure.
    611          */
    612         TestMethod(String name, String description, String controller, String knownFailure,
    613                 boolean isBroken, boolean isSuppressed) {
    614             mName = name;
    615             mDescription = description;
    616             mController = controller;
    617             mKnownFailure = knownFailure;
    618             mIsBroken = isBroken;
    619             mIsSuppressed = isSuppressed;
    620         }
    621     }
    622 }
    623