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