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