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