1/* 2 * Copyright (C) 2010 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 17package org.apache.harmony.xml; 18 19import junit.framework.Assert; 20import junit.framework.AssertionFailedError; 21import junit.framework.Test; 22import junit.framework.TestCase; 23import junit.framework.TestSuite; 24import junit.textui.TestRunner; 25import org.w3c.dom.Attr; 26import org.w3c.dom.Document; 27import org.w3c.dom.Element; 28import org.w3c.dom.EntityReference; 29import org.w3c.dom.NamedNodeMap; 30import org.w3c.dom.Node; 31import org.w3c.dom.NodeList; 32import org.w3c.dom.ProcessingInstruction; 33import org.xml.sax.InputSource; 34import org.xml.sax.SAXException; 35import org.xml.sax.SAXParseException; 36import org.xmlpull.v1.XmlPullParserException; 37import org.xmlpull.v1.XmlPullParserFactory; 38import org.xmlpull.v1.XmlSerializer; 39 40import javax.xml.parsers.DocumentBuilder; 41import javax.xml.parsers.DocumentBuilderFactory; 42import javax.xml.parsers.ParserConfigurationException; 43import javax.xml.transform.ErrorListener; 44import javax.xml.transform.Result; 45import javax.xml.transform.Source; 46import javax.xml.transform.Transformer; 47import javax.xml.transform.TransformerConfigurationException; 48import javax.xml.transform.TransformerException; 49import javax.xml.transform.TransformerFactory; 50import javax.xml.transform.dom.DOMResult; 51import javax.xml.transform.stream.StreamResult; 52import javax.xml.transform.stream.StreamSource; 53import java.io.BufferedInputStream; 54import java.io.File; 55import java.io.FileInputStream; 56import java.io.FileNotFoundException; 57import java.io.IOException; 58import java.io.InputStream; 59import java.io.InputStreamReader; 60import java.io.Reader; 61import java.io.StringReader; 62import java.io.StringWriter; 63import java.util.ArrayList; 64import java.util.Collections; 65import java.util.Comparator; 66import java.util.List; 67 68/** 69 * The <a href="http://www.oasis-open.org/committees/tc_home.php?wg_abbrev=xslt">OASIS 70 * XSLT conformance test suite</a>, adapted for use by JUnit. To run these tests 71 * on a device: 72 * <ul> 73 * <li>Obtain the <a href="http://www.oasis-open.org/committees/download.php/12171/XSLT-testsuite-04.ZIP">test 74 * suite zip file from the OASIS project site.</li> 75 * <li>Unzip. 76 * <li>Copy the files to a device: <code>adb shell mkdir /data/oasis ; 77 * adb push ./XSLT-Conformance-TC /data/oasis</code>. 78 * <li>Invoke this class' main method, passing the on-device path to the test 79 * suite's <code>catalog.xml</code> file as an argument. 80 * </ul> 81 * 82 * <p>Unfortunately, some of the tests in the OASIS suite will fail when 83 * executed outside of their original development environment: 84 * <ul> 85 * <li>The tests assume case insensitive filesystems. Some will fail with 86 * "Couldn't open file" errors due to a mismatch in file name casing. 87 * <li>The tests assume certain network hosts will exist and serve 88 * stylesheet files. In particular, "http://webxtest/" isn't generally 89 * available. 90 * </ul> 91 */ 92public class XsltXPathConformanceTestSuite { 93 94 private static final String defaultCatalogFile 95 = "/home/dalvik-prebuild/OASIS/XSLT-Conformance-TC/TESTS/catalog.xml"; 96 97 /** Orders element attributes by optional URI and name. */ 98 private static final Comparator<Attr> orderByName = new Comparator<Attr>() { 99 public int compare(Attr a, Attr b) { 100 int result = compareNullsFirst(a.getNamespaceURI(), b.getNamespaceURI()); 101 return result == 0 ? result 102 : compareNullsFirst(a.getName(), b.getName()); 103 } 104 105 <T extends Comparable<T>> int compareNullsFirst(T a, T b) { 106 return (a == b) ? 0 107 : (a == null) ? -1 108 : (b == null) ? 1 109 : a.compareTo(b); 110 } 111 }; 112 113 private final DocumentBuilder documentBuilder; 114 private final TransformerFactory transformerFactory; 115 private final XmlPullParserFactory xmlPullParserFactory; 116 117 public XsltXPathConformanceTestSuite() 118 throws ParserConfigurationException, XmlPullParserException { 119 DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); 120 factory.setNamespaceAware(true); 121 factory.setCoalescing(true); 122 documentBuilder = factory.newDocumentBuilder(); 123 124 transformerFactory = TransformerFactory.newInstance(); 125 xmlPullParserFactory = XmlPullParserFactory.newInstance(); 126 } 127 128 public static void main(String[] args) throws Exception { 129 if (args.length != 1) { 130 System.out.println("Usage: XsltXPathConformanceTestSuite <catalog-xml>"); 131 System.out.println(); 132 System.out.println(" catalog-xml: an XML file describing an OASIS test suite"); 133 System.out.println(" such as: " + defaultCatalogFile); 134 return; 135 } 136 137 File catalogXml = new File(args[0]); 138 TestRunner.run(suite(catalogXml)); 139 } 140 141 public static Test suite() throws Exception { 142 return suite(new File(defaultCatalogFile)); 143 } 144 145 /** 146 * Returns a JUnit test suite for the tests described by the given document. 147 */ 148 public static Test suite(File catalogXml) throws Exception { 149 XsltXPathConformanceTestSuite suite = new XsltXPathConformanceTestSuite(); 150 151 /* 152 * Extract the tests from an XML document with the following structure: 153 * 154 * <test-suite> 155 * <test-catalog submitter="Lotus"> 156 * <creator>Lotus/IBM</creator> 157 * <major-path>Xalan_Conformance_Tests</major-path> 158 * <date>2001-11-16</date> 159 * <test-case ...> ... </test-case> 160 * <test-case ...> ... </test-case> 161 * <test-case ...> ... </test-case> 162 * </test-catalog> 163 * </test-suite> 164 */ 165 166 Document document = DocumentBuilderFactory.newInstance() 167 .newDocumentBuilder().parse(catalogXml); 168 Element testSuiteElement = document.getDocumentElement(); 169 TestSuite result = new TestSuite(); 170 for (Element testCatalog : elementsOf(testSuiteElement.getElementsByTagName("test-catalog"))) { 171 Element majorPathElement = (Element) testCatalog.getElementsByTagName("major-path").item(0); 172 String majorPath = majorPathElement.getTextContent(); 173 File base = new File(catalogXml.getParentFile(), majorPath); 174 175 for (Element testCaseElement : elementsOf(testCatalog.getElementsByTagName("test-case"))) { 176 result.addTest(suite.create(base, testCaseElement)); 177 } 178 } 179 180 return result; 181 } 182 183 /** 184 * Returns a JUnit test for the test described by the given element. 185 */ 186 private TestCase create(File base, Element testCaseElement) { 187 188 /* 189 * Extract the XSLT test from a DOM entity with the following structure: 190 * 191 * <test-case category="XSLT-Result-Tree" id="attribset_attribset01"> 192 * <file-path>attribset</file-path> 193 * <creator>Paul Dick</creator> 194 * <date>2001-11-08</date> 195 * <purpose>Set attribute of a LRE from single attribute set.</purpose> 196 * <spec-citation place="7.1.4" type="section" version="1.0" spec="xslt"/> 197 * <scenario operation="standard"> 198 * <input-file role="principal-data">attribset01.xml</input-file> 199 * <input-file role="principal-stylesheet">attribset01.xsl</input-file> 200 * <output-file role="principal" compare="XML">attribset01.out</output-file> 201 * </scenario> 202 * </test-case> 203 */ 204 205 Element filePathElement = (Element) testCaseElement.getElementsByTagName("file-path").item(0); 206 Element purposeElement = (Element) testCaseElement.getElementsByTagName("purpose").item(0); 207 Element specCitationElement = (Element) testCaseElement.getElementsByTagName("spec-citation").item(0); 208 Element scenarioElement = (Element) testCaseElement.getElementsByTagName("scenario").item(0); 209 210 String category = testCaseElement.getAttribute("category"); 211 String id = testCaseElement.getAttribute("id"); 212 String name = category + "." + id; 213 String purpose = purposeElement != null ? purposeElement.getTextContent() : ""; 214 String spec = "place=" + specCitationElement.getAttribute("place") 215 + " type" + specCitationElement.getAttribute("type") 216 + " version=" + specCitationElement.getAttribute("version") 217 + " spec=" + specCitationElement.getAttribute("spec"); 218 String operation = scenarioElement.getAttribute("operation"); 219 220 Element principalDataElement = null; 221 Element principalStylesheetElement = null; 222 Element principalElement = null; 223 224 for (Element element : elementsOf(scenarioElement.getChildNodes())) { 225 String role = element.getAttribute("role"); 226 if (role.equals("principal-data")) { 227 principalDataElement = element; 228 } else if (role.equals("principal-stylesheet")) { 229 principalStylesheetElement = element; 230 } else if (role.equals("principal")) { 231 principalElement = element; 232 } else if (!role.equals("supplemental-stylesheet") 233 && !role.equals("supplemental-data")) { 234 return new MisspecifiedTest("Unexpected element at " + name); 235 } 236 } 237 238 String testDirectory = filePathElement.getTextContent(); 239 File inBase = new File(base, testDirectory); 240 File outBase = new File(new File(base, "REF_OUT"), testDirectory); 241 242 if (principalDataElement == null || principalStylesheetElement == null) { 243 return new MisspecifiedTest("Expected <scenario> to have " 244 + "principal=data and principal-stylesheet elements at " + name); 245 } 246 247 try { 248 File principalData = findFile(inBase, principalDataElement.getTextContent()); 249 File principalStylesheet = findFile(inBase, principalStylesheetElement.getTextContent()); 250 251 final File principal; 252 final String compareAs; 253 if (!operation.equals("execution-error")) { 254 if (principalElement == null) { 255 return new MisspecifiedTest("Expected <scenario> to have principal element at " + name); 256 } 257 258 principal = findFile(outBase, principalElement.getTextContent()); 259 compareAs = principalElement.getAttribute("compare"); 260 } else { 261 principal = null; 262 compareAs = null; 263 } 264 265 return new XsltTest(category, id, purpose, spec, principalData, 266 principalStylesheet, principal, operation, compareAs); 267 } catch (FileNotFoundException e) { 268 return new MisspecifiedTest(e.getMessage() + " at " + name); 269 } 270 } 271 272 /** 273 * Finds the named file in the named directory. This tries extra hard to 274 * avoid case-insensitive-naming problems, where the requested file is 275 * available in a different casing. 276 */ 277 private File findFile(File directory, String name) throws FileNotFoundException { 278 File file = new File(directory, name); 279 if (file.exists()) { 280 return file; 281 } 282 283 for (String child : directory.list()) { 284 if (child.equalsIgnoreCase(name)) { 285 return new File(directory, child); 286 } 287 } 288 289 throw new FileNotFoundException("Missing file: " + file); 290 } 291 292 /** 293 * Placeholder for a test that couldn't be configured to run properly. 294 */ 295 public class MisspecifiedTest extends TestCase { 296 private final String message; 297 298 MisspecifiedTest(String message) { 299 super("test"); 300 this.message = message; 301 } 302 303 public void test() { 304 fail(message); 305 } 306 } 307 308 /** 309 * Processes an input XML file with an input XSLT stylesheet and compares 310 * the result to an expected output file. 311 */ 312 public class XsltTest extends TestCase { 313 private final String category; 314 private final String id; 315 private final String purpose; 316 private final String spec; 317 318 private final File principalData; 319 private final File principalStylesheet; 320 private final File principal; 321 322 /** either "standard" or "execution-error" */ 323 private final String operation; 324 325 /** 326 * The syntax to compare the output file using, such as "XML", "HTML", 327 * "manual", or null for expected execution errors. 328 */ 329 private final String compareAs; 330 331 XsltTest(String category, String id, String purpose, String spec, 332 File principalData, File principalStylesheet, File principal, 333 String operation, String compareAs) { 334 super("test"); 335 this.category = category; 336 this.id = id; 337 this.purpose = purpose; 338 this.spec = spec; 339 this.principalData = principalData; 340 this.principalStylesheet = principalStylesheet; 341 this.principal = principal; 342 this.operation = operation; 343 this.compareAs = compareAs; 344 } 345 346 XsltTest(File principalData, File principalStylesheet, File principal) { 347 this("standalone", "test", "", "", 348 principalData, principalStylesheet, principal, "standard", "XML"); 349 } 350 351 public void test() throws Exception { 352 if (purpose != null) { 353 System.out.println("Purpose: " + purpose); 354 } 355 if (spec != null) { 356 System.out.println("Spec: " + spec); 357 } 358 359 Result result; 360 if ("XML".equals(compareAs)) { 361 DOMResult domResult = new DOMResult(); 362 domResult.setNode(documentBuilder.newDocument().createElementNS("", "result")); 363 result = domResult; 364 } else { 365 result = new StreamResult(new StringWriter()); 366 } 367 368 ErrorRecorder errorRecorder = new ErrorRecorder(); 369 transformerFactory.setErrorListener(errorRecorder); 370 371 Transformer transformer; 372 try { 373 Source xslt = new StreamSource(principalStylesheet); 374 transformer = transformerFactory.newTransformer(xslt); 375 if (errorRecorder.error == null) { 376 transformer.setErrorListener(errorRecorder); 377 transformer.transform(new StreamSource(principalData), result); 378 } 379 } catch (TransformerConfigurationException e) { 380 errorRecorder.fatalError(e); 381 } 382 383 if (operation.equals("standard")) { 384 if (errorRecorder.error != null) { 385 throw errorRecorder.error; 386 } 387 } else if (operation.equals("execution-error")) { 388 if (errorRecorder.error != null) { 389 return; 390 } 391 fail("Expected " + operation + ", but transform completed normally." 392 + " (Warning=" + errorRecorder.warning + ")"); 393 } else { 394 throw new UnsupportedOperationException("Unexpected operation: " + operation); 395 } 396 397 if ("XML".equals(compareAs)) { 398 assertNodesAreEquivalent(principal, ((DOMResult) result).getNode()); 399 } else { 400 // TODO: implement support for comparing HTML etc. 401 throw new UnsupportedOperationException("Cannot compare as " + compareAs); 402 } 403 } 404 405 @Override public String getName() { 406 return category + "." + id; 407 } 408 } 409 410 /** 411 * Ensures both XML documents represent the same semantic data. Non-semantic 412 * data such as namespace prefixes, comments, and whitespace is ignored. 413 * 414 * @param actual an XML document whose root is a {@code <result>} element. 415 * @param expected a file containing an XML document fragment. 416 */ 417 private void assertNodesAreEquivalent(File expected, Node actual) 418 throws ParserConfigurationException, IOException, SAXException, 419 XmlPullParserException { 420 421 Node expectedNode = fileToResultNode(expected); 422 String expectedString = nodeToNormalizedString(expectedNode); 423 String actualString = nodeToNormalizedString(actual); 424 425 Assert.assertEquals("Expected XML to match file " + expected, 426 expectedString, actualString); 427 } 428 429 /** 430 * Returns the given file's XML fragment as a single node, wrapped in 431 * {@code <result>} tags. This takes care of normalizing the following 432 * conditions: 433 * 434 * <ul> 435 * <li>Files containing XML document fragments with multiple elements: 436 * {@code <SPAN style="color=blue">Smurfs!</SPAN><br />} 437 * 438 * <li>Files containing XML document fragments with no elements: 439 * {@code Smurfs!} 440 * 441 * <li>Files containing proper XML documents with a single element and an 442 * XML declaration: 443 * {@code <?xml version="1.0"?><doc />} 444 * 445 * <li>Files prefixed with a byte order mark header, such as 0xEFBBBF. 446 * </ul> 447 */ 448 private Node fileToResultNode(File file) throws IOException, SAXException { 449 String rawContents = fileToString(file); 450 String fragment = rawContents; 451 452 // If the file had an XML declaration, strip that. Otherwise wrapping 453 // it in <result> tags would result in a malformed XML document. 454 if (fragment.startsWith("<?xml")) { 455 int declarationEnd = fragment.indexOf("?>"); 456 fragment = fragment.substring(declarationEnd + 2); 457 } 458 459 // Parse it as document fragment wrapped in <result> tags. 460 try { 461 fragment = "<result>" + fragment + "</result>"; 462 return documentBuilder.parse(new InputSource(new StringReader(fragment))) 463 .getDocumentElement(); 464 } catch (SAXParseException e) { 465 Error error = new AssertionFailedError( 466 "Failed to parse XML: " + file + "\n" + rawContents); 467 error.initCause(e); 468 throw error; 469 } 470 } 471 472 private String nodeToNormalizedString(Node node) 473 throws XmlPullParserException, IOException { 474 StringWriter writer = new StringWriter(); 475 XmlSerializer xmlSerializer = xmlPullParserFactory.newSerializer(); 476 xmlSerializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true); 477 xmlSerializer.setOutput(writer); 478 emitNode(xmlSerializer, node); 479 xmlSerializer.flush(); 480 return writer.toString(); 481 } 482 483 private void emitNode(XmlSerializer serializer, Node node) throws IOException { 484 if (node == null) { 485 throw new UnsupportedOperationException("Cannot emit null nodes"); 486 487 } else if (node.getNodeType() == Node.ELEMENT_NODE) { 488 Element element = (Element) node; 489 serializer.startTag(element.getNamespaceURI(), element.getLocalName()); 490 emitAttributes(serializer, element); 491 emitChildren(serializer, element); 492 serializer.endTag(element.getNamespaceURI(), element.getLocalName()); 493 494 } else if (node.getNodeType() == Node.TEXT_NODE 495 || node.getNodeType() == Node.CDATA_SECTION_NODE) { 496 // TODO: is it okay to trim whitespace in general? This may cause 497 // false positives for elements like HTML's <pre> tag 498 String trimmed = node.getTextContent().trim(); 499 if (trimmed.length() > 0) { 500 serializer.text(trimmed); 501 } 502 503 } else if (node.getNodeType() == Node.DOCUMENT_NODE) { 504 Document document = (Document) node; 505 serializer.startDocument("UTF-8", true); 506 emitNode(serializer, document.getDocumentElement()); 507 serializer.endDocument(); 508 509 } else if (node.getNodeType() == Node.PROCESSING_INSTRUCTION_NODE) { 510 ProcessingInstruction processingInstruction = (ProcessingInstruction) node; 511 String data = processingInstruction.getData(); 512 String target = processingInstruction.getTarget(); 513 serializer.processingInstruction(target + " " + data); 514 515 } else if (node.getNodeType() == Node.COMMENT_NODE) { 516 // ignore! 517 518 } else if (node.getNodeType() == Node.ENTITY_REFERENCE_NODE) { 519 EntityReference entityReference = (EntityReference) node; 520 serializer.entityRef(entityReference.getNodeName()); 521 522 } else { 523 throw new UnsupportedOperationException( 524 "Cannot emit " + node + " of type " + node.getNodeType()); 525 } 526 } 527 528 private void emitAttributes(XmlSerializer serializer, Node node) 529 throws IOException { 530 NamedNodeMap map = node.getAttributes(); 531 if (map == null) { 532 return; 533 } 534 535 List<Attr> attributes = new ArrayList<Attr>(); 536 for (int i = 0; i < map.getLength(); i++) { 537 attributes.add((Attr) map.item(i)); 538 } 539 Collections.sort(attributes, orderByName); 540 541 for (Attr attr : attributes) { 542 if ("xmlns".equals(attr.getPrefix()) || "xmlns".equals(attr.getLocalName())) { 543 /* 544 * Omit namespace declarations because they aren't considered 545 * data. Ie. <foo:a xmlns:bar="http://google.com"> is semantically 546 * equal to <bar:a xmlns:bar="http://google.com"> since the 547 * prefix doesn't matter, only the URI it points to. 548 * 549 * When we omit the prefix, our XML serializer will still 550 * generate one for us, using a predictable pattern. 551 */ 552 } else { 553 serializer.attribute(attr.getNamespaceURI(), attr.getLocalName(), attr.getValue()); 554 } 555 } 556 } 557 558 private void emitChildren(XmlSerializer serializer, Node node) 559 throws IOException { 560 NodeList childNodes = node.getChildNodes(); 561 for (int i = 0; i < childNodes.getLength(); i++) { 562 emitNode(serializer, childNodes.item(i)); 563 } 564 } 565 566 private static List<Element> elementsOf(NodeList nodeList) { 567 List<Element> result = new ArrayList<Element>(); 568 for (int i = 0; i < nodeList.getLength(); i++) { 569 Node node = nodeList.item(i); 570 if (node instanceof Element) { 571 result.add((Element) node); 572 } 573 } 574 return result; 575 } 576 577 /** 578 * Reads the given file into a string. If the file contains a byte order 579 * mark, the corresponding character set will be used. Otherwise the system 580 * default charset will be used. 581 */ 582 private String fileToString(File file) throws IOException { 583 InputStream in = new BufferedInputStream(new FileInputStream(file), 1024); 584 585 // Read the byte order mark to determine the charset. 586 // TODO: use a built-in API for this... 587 Reader reader; 588 in.mark(3); 589 int byte1 = in.read(); 590 int byte2 = in.read(); 591 if (byte1 == 0xFF && byte2 == 0xFE) { 592 reader = new InputStreamReader(in, "UTF-16LE"); 593 } else if (byte1 == 0xFF && byte2 == 0xFF) { 594 reader = new InputStreamReader(in, "UTF-16BE"); 595 } else { 596 int byte3 = in.read(); 597 if (byte1 == 0xEF && byte2 == 0xBB && byte3 == 0xBF) { 598 reader = new InputStreamReader(in, "UTF-8"); 599 } else { 600 in.reset(); 601 reader = new InputStreamReader(in); 602 } 603 } 604 605 StringWriter out = new StringWriter(); 606 char[] buffer = new char[1024]; 607 int count; 608 while ((count = reader.read(buffer)) != -1) { 609 out.write(buffer, 0, count); 610 } 611 return out.toString(); 612 } 613 614 static class ErrorRecorder implements ErrorListener { 615 Exception warning; 616 Exception error; 617 618 public void warning(TransformerException exception) { 619 if (this.warning == null) { 620 this.warning = exception; 621 } 622 } 623 624 public void error(TransformerException exception) { 625 if (this.error == null) { 626 this.error = exception; 627 } 628 } 629 630 public void fatalError(TransformerException exception) { 631 if (this.error == null) { 632 this.error = exception; 633 } 634 } 635 } 636} 637