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