You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
668 lines
23 KiB
668 lines
23 KiB
/* |
|
* 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; |
|
} |
|
} |
|
}
|
|
|