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