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