• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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      */
start(RootDoc root)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      */
optionLength(String option)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      */
isValidJUnitTestCase(ClassDoc clazz)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          */
e(String msg, Exception e)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          */
t(String msg)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          */
initTrace(String name)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          */
closeTrace()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 
XMLGenerator(String outputPath)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 
addTestClass(TestClass tc)268         void addTestClass(TestClass tc) {
269             appendSuiteToElement(mDoc.getDocumentElement(), tc);
270         }
271 
dump()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          */
rebuildDocument()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          */
mergeEmptySuites(Node suiteElem)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          */
getUnmutableChildNodes(Node node)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          */
appendSuiteToElement(Node elem, TestClass testSuite)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          */
getSuiteChildren(Node elem)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          */
appendTestCases(Node elem, Collection<TestMethod> cases)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          */
setAttribute(Node elem, String name, String value)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          */
getAttribute(Node elem, String name)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          */
appendSuiteToElementImpl(Node elem, TestClass testSuite)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          */
TestClass(String name, Collection<TestMethod> cases)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          */
TestClass(ClassDoc clazz, ExpectationStore expectationStore)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          */
getTestMethods(ExpectationStore expectationStore, ClassDoc clazz)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          */
getAnnotationDescription(AnnotationDesc cAnnot)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          */
getAllMethods(ClassDoc clazz)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          */
TestMethod(String name, String description, String controller, String knownFailure, boolean isBroken, boolean isSuppressed)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