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