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