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