/*
* 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;
}
}
}