/* * Copyright (C) The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import java.io.BufferedWriter; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.FileWriter; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import java.util.Set; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerFactory; import javax.xml.transform.TransformerFactoryConfigurationError; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import org.w3c.dom.Attr; import org.w3c.dom.Document; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import vogar.ExpectationStore; import vogar.Expectation; import com.sun.javadoc.AnnotationDesc; import com.sun.javadoc.AnnotationTypeDoc; import com.sun.javadoc.AnnotationValue; import com.sun.javadoc.ClassDoc; import com.sun.javadoc.Doclet; import com.sun.javadoc.MethodDoc; import com.sun.javadoc.RootDoc; import com.sun.javadoc.AnnotationDesc.ElementValuePair; /** * This is only a very simple and brief JavaDoc parser for the CTS. * * Input: The source files of the test cases. It will be represented * as a list of ClassDoc * Output: Generate file description.xml, which defines the TestPackage * TestSuite and TestCases. * * Note: * 1. Since this class has dependencies on com.sun.javadoc package which * is not implemented on Android. So this class can't be compiled. * 2. The TestSuite can be embedded, which means: * TestPackage := TestSuite* * TestSuite := TestSuite* | TestCase* */ public class DescriptionGenerator extends Doclet { static final String HOST_CONTROLLER = "dalvik.annotation.HostController"; static final String KNOWN_FAILURE = "dalvik.annotation.KnownFailure"; static final String SUPPRESSED_TEST = "android.test.suitebuilder.annotation.Suppress"; static final String CTS_EXPECTATION_DIR = "cts/tests/expectations"; static final String JUNIT_TEST_CASE_CLASS_NAME = "junit.framework.testcase"; static final String TAG_PACKAGE = "TestPackage"; static final String TAG_SUITE = "TestSuite"; static final String TAG_CASE = "TestCase"; static final String TAG_TEST = "Test"; static final String TAG_DESCRIPTION = "Description"; static final String ATTRIBUTE_NAME_VERSION = "version"; static final String ATTRIBUTE_VALUE_VERSION = "1.0"; static final String ATTRIBUTE_NAME_FRAMEWORK = "AndroidFramework"; static final String ATTRIBUTE_VALUE_FRAMEWORK = "Android 1.0"; static final String ATTRIBUTE_NAME = "name"; static final String ATTRIBUTE_ABIS = "abis"; static final String ATTRIBUTE_HOST_CONTROLLER = "HostController"; static final String ATTRIBUTE_TIMEOUT = "timeout"; static final String XML_OUTPUT_PATH = "./description.xml"; static final String OUTPUT_PATH_OPTION = "-o"; static final String ARCHITECTURE_OPTION = "-a"; /** * Start to parse the classes passed in by javadoc, and generate * the xml file needed by CTS packer. * * @param root The root document passed in by javadoc. * @return Whether the document has been processed. */ public static boolean start(RootDoc root) { ClassDoc[] classes = root.classes(); if (classes == null) { Log.e("No class found!", null); return true; } String outputPath = XML_OUTPUT_PATH; String architecture = null; String[][] options = root.options(); for (String[] option : options) { if (option.length == 2) { if (option[0].equals(OUTPUT_PATH_OPTION)) { outputPath = option[1]; } else if (option[0].equals(ARCHITECTURE_OPTION)) { architecture = option[1]; } } } if (architecture == null || architecture.equals("")) { Log.e("Missing architecture!", null); return false; } XMLGenerator xmlGenerator = null; try { xmlGenerator = new XMLGenerator(outputPath); } catch (ParserConfigurationException e) { Log.e("Cant initialize XML Generator!", e); return true; } ExpectationStore ctsExpectationStore = null; try { ctsExpectationStore = VogarUtils.provideExpectationStore("./" + CTS_EXPECTATION_DIR); } catch (IOException e) { Log.e("Couldn't load expectation store.", e); return false; } for (ClassDoc clazz : classes) { if ((!clazz.isAbstract()) && (isValidJUnitTestCase(clazz))) { xmlGenerator.addTestClass(new TestClass(clazz, ctsExpectationStore, architecture)); } } try { xmlGenerator.dump(); } catch (Exception e) { Log.e("Can't dump to XML file!", e); } return true; } /** * Return the length of any doclet options we recognize * @param option The option name * @return The number of words this option takes (including the option) or 0 if the option * is not recognized. */ public static int optionLength(String option) { if (option.equals(OUTPUT_PATH_OPTION)) { return 2; } return 0; } /** * Check if the class is valid test case inherited from JUnit TestCase. * * @param clazz The class to be checked. * @return If the class is valid test case inherited from JUnit TestCase, return true; * else, return false. */ static boolean isValidJUnitTestCase(ClassDoc clazz) { while((clazz = clazz.superclass()) != null) { if (JUNIT_TEST_CASE_CLASS_NAME.equals(clazz.qualifiedName().toLowerCase())) { return true; } } return false; } /** * Log utility. */ static class Log { private static boolean TRACE = true; private static BufferedWriter mTraceOutput = null; /** * Log the specified message. * * @param msg The message to be logged. */ static void e(String msg, Exception e) { System.out.println(msg); if (e != null) { e.printStackTrace(); } } /** * Add the message to the trace stream. * * @param msg The message to be added to the trace stream. */ public static void t(String msg) { if (TRACE) { try { if ((mTraceOutput != null) && (msg != null)) { mTraceOutput.write(msg + "\n"); mTraceOutput.flush(); } } catch (IOException e) { e.printStackTrace(); } } } /** * Initialize the trace stream. * * @param name The class name. */ public static void initTrace(String name) { if (TRACE) { try { if (mTraceOutput == null) { String fileName = "cts_debug_dg_" + name + ".txt"; mTraceOutput = new BufferedWriter(new FileWriter(fileName)); } } catch (IOException e) { e.printStackTrace(); } } } /** * Close the trace stream. */ public static void closeTrace() { if (mTraceOutput != null) { try { mTraceOutput.close(); mTraceOutput = null; } catch (IOException e) { e.printStackTrace(); } } } } static class XMLGenerator { String mOutputPath; /** * This document is used to represent the description XML file. * It is construct by the classes passed in, which contains the * information of all the test package, test suite and test cases. */ Document mDoc; XMLGenerator(String outputPath) throws ParserConfigurationException { mOutputPath = outputPath; mDoc = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument(); Node testPackageElem = mDoc.appendChild(mDoc.createElement(TAG_PACKAGE)); setAttribute(testPackageElem, ATTRIBUTE_NAME_VERSION, ATTRIBUTE_VALUE_VERSION); setAttribute(testPackageElem, ATTRIBUTE_NAME_FRAMEWORK, ATTRIBUTE_VALUE_FRAMEWORK); } void addTestClass(TestClass tc) { appendSuiteToElement(mDoc.getDocumentElement(), tc); } void dump() throws TransformerFactoryConfigurationError, FileNotFoundException, TransformerException { //rebuildDocument(); Transformer t = TransformerFactory.newInstance().newTransformer(); // enable indent in result file t.setOutputProperty("indent", "yes"); t.setOutputProperty("{http://xml.apache.org/xslt}indent-amount","4"); File file = new File(mOutputPath); file.getParentFile().mkdirs(); t.transform(new DOMSource(mDoc), new StreamResult(new FileOutputStream(file))); } /** * Rebuild the document, merging empty suite nodes. */ void rebuildDocument() { // merge empty suite nodes Collection<Node> suiteElems = getUnmutableChildNodes(mDoc.getDocumentElement()); Iterator<Node> suiteIterator = suiteElems.iterator(); while (suiteIterator.hasNext()) { Node suiteElem = suiteIterator.next(); mergeEmptySuites(suiteElem); } } /** * Merge the test suite which only has one sub-suite. In this case, unify * the name of the two test suites. * * @param suiteElem The suite element of which to be merged. */ void mergeEmptySuites(Node suiteElem) { Collection<Node> suiteChildren = getSuiteChildren(suiteElem); if (suiteChildren.size() > 1) { for (Node suiteChild : suiteChildren) { mergeEmptySuites(suiteChild); } } else if (suiteChildren.size() == 1) { // do merge Node child = suiteChildren.iterator().next(); // update name String newName = getAttribute(suiteElem, ATTRIBUTE_NAME) + "." + getAttribute(child, ATTRIBUTE_NAME); setAttribute(child, ATTRIBUTE_NAME, newName); // update parent node Node parentNode = suiteElem.getParentNode(); parentNode.removeChild(suiteElem); parentNode.appendChild(child); mergeEmptySuites(child); } } /** * Get the unmuatable child nodes for specified node. * * @param node The specified node. * @return A collection of copied child node. */ private Collection<Node> getUnmutableChildNodes(Node node) { ArrayList<Node> nodes = new ArrayList<Node>(); NodeList nodelist = node.getChildNodes(); for (int i = 0; i < nodelist.getLength(); i++) { nodes.add(nodelist.item(i)); } return nodes; } /** * Append a named test suite to a specified element. Including match with * the existing suite nodes and do the real creation and append. * * @param elem The specified element. * @param testSuite The test suite to be appended. */ void appendSuiteToElement(Node elem, TestClass testSuite) { String suiteName = testSuite.mName; Collection<Node> children = getSuiteChildren(elem); int dotIndex = suiteName.indexOf('.'); String name = dotIndex == -1 ? suiteName : suiteName.substring(0, dotIndex); boolean foundMatch = false; for (Node child : children) { String childName = child.getAttributes().getNamedItem(ATTRIBUTE_NAME) .getNodeValue(); if (childName.equals(name)) { foundMatch = true; if (dotIndex == -1) { appendTestCases(child, testSuite.mCases); } else { testSuite.mName = suiteName.substring(dotIndex + 1, suiteName.length()); appendSuiteToElement(child, testSuite); } } } if (!foundMatch) { appendSuiteToElementImpl(elem, testSuite); } } /** * Get the test suite child nodes of a specified element. * * @param elem The specified element node. * @return The matched child nodes. */ Collection<Node> getSuiteChildren(Node elem) { ArrayList<Node> suites = new ArrayList<Node>(); NodeList children = elem.getChildNodes(); for (int i = 0; i < children.getLength(); i++) { Node child = children.item(i); if (child.getNodeName().equals(DescriptionGenerator.TAG_SUITE)) { suites.add(child); } } return suites; } /** * Create test case node according to the given method names, and append them * to the test suite element. * * @param elem The test suite element. * @param cases A collection of test cases included by the test suite class. */ void appendTestCases(Node elem, Collection<TestMethod> cases) { if (cases.isEmpty()) { // if no method, remove from parent elem.getParentNode().removeChild(elem); } else { for (TestMethod caze : cases) { if (caze.mIsBroken || caze.mIsSuppressed || caze.mKnownFailure != null) { continue; } Node caseNode = elem.appendChild(mDoc.createElement(TAG_TEST)); setAttribute(caseNode, ATTRIBUTE_NAME, caze.mName); String abis = caze.mAbis.toString(); setAttribute(caseNode, ATTRIBUTE_ABIS, abis.substring(1, abis.length() - 1)); if ((caze.mController != null) && (caze.mController.length() != 0)) { setAttribute(caseNode, ATTRIBUTE_HOST_CONTROLLER, caze.mController); } if (caze.mTimeoutInMinutes != 0) { setAttribute(caseNode, ATTRIBUTE_TIMEOUT, Integer.toString(caze.mTimeoutInMinutes)); } if (caze.mDescription != null && !caze.mDescription.equals("")) { caseNode.appendChild(mDoc.createElement(TAG_DESCRIPTION)) .setTextContent(caze.mDescription); } } } } /** * Set the attribute of element. * * @param elem The element to be set attribute. * @param name The attribute name. * @param value The attribute value. */ protected void setAttribute(Node elem, String name, String value) { Attr attr = mDoc.createAttribute(name); attr.setNodeValue(value); elem.getAttributes().setNamedItem(attr); } /** * Get the value of a specified attribute of an element. * * @param elem The element node. * @param name The attribute name. * @return The value of the specified attribute. */ private String getAttribute(Node elem, String name) { return elem.getAttributes().getNamedItem(name).getNodeValue(); } /** * Do the append, including creating test suite nodes and test case nodes, and * append them to the element. * * @param elem The specified element node. * @param testSuite The test suite to be append. */ void appendSuiteToElementImpl(Node elem, TestClass testSuite) { Node parent = elem; String suiteName = testSuite.mName; int dotIndex; while ((dotIndex = suiteName.indexOf('.')) != -1) { String name = suiteName.substring(0, dotIndex); Node suiteElem = parent.appendChild(mDoc.createElement(TAG_SUITE)); setAttribute(suiteElem, ATTRIBUTE_NAME, name); parent = suiteElem; suiteName = suiteName.substring(dotIndex + 1, suiteName.length()); } Node leafSuiteElem = parent.appendChild(mDoc.createElement(TAG_CASE)); setAttribute(leafSuiteElem, ATTRIBUTE_NAME, suiteName); appendTestCases(leafSuiteElem, testSuite.mCases); } } /** * Represent the test class. */ static class TestClass { String mName; Collection<TestMethod> mCases; /** * Construct an test suite object. * * @param name Full name of the test suite, such as "com.google.android.Foo" * @param cases The test cases included in this test suite. */ TestClass(String name, Collection<TestMethod> cases) { mName = name; mCases = cases; } /** * Construct a TestClass object using ClassDoc. * * @param clazz The specified ClassDoc. */ TestClass(ClassDoc clazz, ExpectationStore expectationStore, String architecture) { mName = clazz.toString(); mCases = getTestMethods(expectationStore, architecture, clazz); } /** * Get all the TestMethod from a ClassDoc, including inherited methods. * * @param clazz The specified ClassDoc. * @return A collection of TestMethod. */ Collection<TestMethod> getTestMethods(ExpectationStore expectationStore, String architecture, ClassDoc clazz) { Collection<MethodDoc> methods = getAllMethods(clazz); ArrayList<TestMethod> cases = new ArrayList<TestMethod>(); Iterator<MethodDoc> iterator = methods.iterator(); while (iterator.hasNext()) { MethodDoc method = iterator.next(); String name = method.name(); AnnotationDesc[] annotations = method.annotations(); String controller = ""; String knownFailure = null; boolean isBroken = false; boolean isSuppressed = false; for (AnnotationDesc cAnnot : annotations) { AnnotationTypeDoc atype = cAnnot.annotationType(); if (atype.toString().equals(HOST_CONTROLLER)) { controller = getAnnotationDescription(cAnnot); } else if (atype.toString().equals(KNOWN_FAILURE)) { knownFailure = getAnnotationDescription(cAnnot); } else if (atype.toString().equals(SUPPRESSED_TEST)) { isSuppressed = true; } } if (VogarUtils.isVogarKnownFailure(expectationStore, clazz.toString(), name)) { isBroken = true; } if (name.startsWith("test")) { Expectation expectation = expectationStore.get( VogarUtils.buildFullTestName(clazz.toString(), name)); Set<String> supportedAbis = VogarUtils.extractSupportedAbis(architecture, expectation); int timeoutInMinutes = VogarUtils.timeoutInMinutes(expectation); cases.add(new TestMethod( name, method.commentText(), controller, supportedAbis, knownFailure, isBroken, isSuppressed, timeoutInMinutes)); } } return cases; } /** * Get annotation description. * * @param cAnnot The annotation. */ String getAnnotationDescription(AnnotationDesc cAnnot) { ElementValuePair[] cpairs = cAnnot.elementValues(); ElementValuePair evp = cpairs[0]; AnnotationValue av = evp.value(); String description = av.toString(); // FIXME: need to find out the reason why there are leading and trailing " description = description.substring(1, description.length() -1); return description; } /** * Get all MethodDoc of a ClassDoc, including inherited methods. * * @param clazz The specified ClassDoc. * @return A collection of MethodDoc. */ Collection<MethodDoc> getAllMethods(ClassDoc clazz) { ArrayList<MethodDoc> methods = new ArrayList<MethodDoc>(); for (MethodDoc method : clazz.methods()) { methods.add(method); } ClassDoc superClass = clazz.superclass(); while (superClass != null) { for (MethodDoc method : superClass.methods()) { methods.add(method); } superClass = superClass.superclass(); } return methods; } } /** * Represent the test method inside the test class. */ static class TestMethod { String mName; String mDescription; String mController; Set<String> mAbis; String mKnownFailure; boolean mIsBroken; boolean mIsSuppressed; int mTimeoutInMinutes; // zero to use default timeout. /** * Construct an test case object. * * @param name The name of the test case. * @param description The description of the test case. * @param knownFailure The reason of known failure. */ TestMethod(String name, String description, String controller, Set<String> abis, String knownFailure, boolean isBroken, boolean isSuppressed, int timeoutInMinutes) { if (timeoutInMinutes < 0) { throw new IllegalArgumentException("timeoutInMinutes < 0: " + timeoutInMinutes); } mName = name; mDescription = description; mController = controller; mAbis = abis; mKnownFailure = knownFailure; mIsBroken = isBroken; mIsSuppressed = isSuppressed; mTimeoutInMinutes = timeoutInMinutes; } } }