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