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      */
start(RootDoc root)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      */
optionLength(String option)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      */
isValidJUnitTestCase(ClassDoc clazz)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          */
e(String msg, Exception e)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          */
t(String msg)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          */
initTrace(String name)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          */
closeTrace()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 
XMLGenerator(String outputPath)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 
addTestClass(TestClass tc)280         void addTestClass(TestClass tc) {
281             appendSuiteToElement(mDoc.getDocumentElement(), tc);
282         }
283 
dump()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          */
rebuildDocument()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          */
mergeEmptySuites(Node suiteElem)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          */
getUnmutableChildNodes(Node node)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          */
appendSuiteToElement(Node elem, TestClass testSuite)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          */
getSuiteChildren(Node elem)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          */
appendTestCases(Node elem, Collection<TestMethod> cases)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          */
setAttribute(Node elem, String name, String value)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          */
getAttribute(Node elem, String name)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          */
appendSuiteToElementImpl(Node elem, TestClass testSuite)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          */
TestClass(String name, Collection<TestMethod> cases)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          */
TestClass(ClassDoc clazz, ExpectationStore expectationStore, String architecture)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          */
getTestMethods(ExpectationStore expectationStore, String architecture, ClassDoc clazz)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          */
getAnnotationDescription(AnnotationDesc cAnnot)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          */
getAllMethods(ClassDoc clazz)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          */
TestMethod(String name, String description, String controller, Set<String> abis, String knownFailure, boolean isBroken, boolean isSuppressed, int timeoutInMinutes)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