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.AssertionFailedError; 20import junit.framework.Test; 21import junit.framework.TestCase; 22import junit.framework.TestSuite; 23import junit.textui.TestRunner; 24import org.w3c.dom.Element; 25import org.w3c.dom.Node; 26import org.w3c.dom.NodeList; 27import org.xml.sax.InputSource; 28import org.xml.sax.SAXException; 29 30import javax.xml.namespace.QName; 31import javax.xml.parsers.DocumentBuilderFactory; 32import javax.xml.parsers.ParserConfigurationException; 33import javax.xml.xpath.XPath; 34import javax.xml.xpath.XPathConstants; 35import javax.xml.xpath.XPathExpressionException; 36import javax.xml.xpath.XPathFactory; 37import javax.xml.xpath.XPathVariableResolver; 38import java.io.File; 39import java.io.IOException; 40import java.util.ArrayList; 41import java.util.List; 42 43/** 44 * The implementation-independent part of the <a 45 * href="http://jaxen.codehaus.org/">Jaxen</a> XPath test suite, adapted for use 46 * by JUnit. To run these tests on a device: 47 * <ul> 48 * <li>Obtain the Jaxen source from the project's website. 49 * <li>Copy the files to a device: <code>adb shell mkdir /data/jaxen ; 50 * adb push /home/dalvik-prebuild/jaxen /data/jaxen</code> 51 * <li>Invoke this class' main method, passing the on-device path to the test 52 * suite's root directory as an argument. 53 * </ul> 54 */ 55public class JaxenXPathTestSuite { 56 57 private static final File DEFAULT_JAXEN_HOME 58 = new File("/home/dalvik-prebuild/jaxen"); 59 60 public static void main(String[] args) throws Exception { 61 if (args.length != 1) { 62 System.out.println("Usage: JaxenXPathTestSuite <jaxen-home>"); 63 return; 64 } 65 66 File jaxenHome = new File(args[0]); 67 TestRunner.run(suite(jaxenHome)); 68 } 69 70 public static Test suite() throws Exception { 71 return suite(DEFAULT_JAXEN_HOME); 72 } 73 74 /** 75 * Creates a test suite from the Jaxen tests.xml catalog. 76 */ 77 public static Test suite(File jaxenHome) 78 throws ParserConfigurationException, IOException, SAXException { 79 80 /* 81 * The tests.xml document has this structure: 82 * 83 * <tests> 84 * <document url="..."> 85 * <context .../> 86 * <context .../> 87 * <context .../> 88 * </document> 89 * <document url="..."> 90 * <context .../> 91 * </document> 92 * </tests> 93 */ 94 95 File testsXml = new File(jaxenHome + "/xml/test/tests.xml"); 96 Element tests = DocumentBuilderFactory.newInstance() 97 .newDocumentBuilder().parse(testsXml).getDocumentElement(); 98 99 TestSuite result = new TestSuite(); 100 for (Element document : elementsOf(tests.getElementsByTagName("document"))) { 101 String url = document.getAttribute("url"); 102 InputSource inputSource = new InputSource("file:" + jaxenHome + "/" + url); 103 for (final Element context : elementsOf(document.getElementsByTagName("context"))) { 104 contextToTestSuite(result, url, inputSource, context); 105 } 106 } 107 108 return result; 109 } 110 111 /** 112 * Populates the test suite with tests from the given XML context element. 113 */ 114 private static void contextToTestSuite(TestSuite suite, String url, 115 InputSource inputSource, Element element) { 116 117 /* 118 * Each context element has this structure: 119 * 120 * <context select="..."> 121 * <test .../> 122 * <test .../> 123 * <test .../> 124 * <valueOf .../> 125 * <valueOf .../> 126 * <valueOf .../> 127 * </context> 128 */ 129 130 String select = element.getAttribute("select"); 131 Context context = new Context(inputSource, url, select); 132 133 XPath xpath = XPathFactory.newInstance().newXPath(); 134 xpath.setXPathVariableResolver(new ElementVariableResolver(element)); 135 136 for (Element test : elementsOf(element.getChildNodes())) { 137 if (test.getTagName().equals("test")) { 138 suite.addTest(createFromTest(xpath, context, test)); 139 140 } else if (test.getTagName().equals("valueOf")) { 141 suite.addTest(createFromValueOf(xpath, context, test)); 142 143 } else { 144 throw new UnsupportedOperationException("Unsupported test: " + context); 145 } 146 } 147 } 148 149 /** 150 * Returns the test described by the given {@code <test>} element. Such 151 * tests come in one of three varieties: 152 * 153 * <ul> 154 * <li>Expected failures. 155 * <li>String matches. These tests have a nested {@code <valueOf>} element 156 * that sub-selects an expected text. 157 * <li>Count matches. These tests specify how many nodes are expected to 158 * match. 159 * </ul> 160 */ 161 private static TestCase createFromTest( 162 final XPath xpath, final Context context, final Element element) { 163 final String select = element.getAttribute("select"); 164 165 /* Such as <test exception="true" select="..." count="0"/> */ 166 if (element.getAttribute("exception").equals("true")) { 167 return new XPathTest(context, select) { 168 @Override void test(Node contextNode) { 169 try { 170 xpath.evaluate(select, contextNode); 171 fail("Expected exception!"); 172 } catch (XPathExpressionException expected) { 173 } 174 } 175 }; 176 } 177 178 /* a <test> with a nested <valueOf>, both of which have select attributes */ 179 NodeList valueOfElements = element.getElementsByTagName("valueOf"); 180 if (valueOfElements.getLength() == 1) { 181 final Element valueOf = (Element) valueOfElements.item(0); 182 final String valueOfSelect = valueOf.getAttribute("select"); 183 184 return new XPathTest(context, select) { 185 @Override void test(Node contextNode) throws XPathExpressionException { 186 Node newContext = (Node) xpath.evaluate( 187 select, contextNode, XPathConstants.NODE); 188 assertEquals(valueOf.getTextContent(), 189 xpath.evaluate(valueOfSelect, newContext, XPathConstants.STRING)); 190 } 191 }; 192 } 193 194 /* Such as <test select="..." count="5"/> */ 195 final String count = element.getAttribute("count"); 196 if (count.length() > 0) { 197 return new XPathTest(context, select) { 198 @Override void test(Node contextNode) throws XPathExpressionException { 199 NodeList result = (NodeList) xpath.evaluate( 200 select, contextNode, XPathConstants.NODESET); 201 assertEquals(Integer.parseInt(count), result.getLength()); 202 } 203 }; 204 } 205 206 throw new UnsupportedOperationException("Unsupported test: " + context); 207 } 208 209 /** 210 * Returns the test described by the given {@code <valueOf>} element. These 211 * tests select an expected text. 212 */ 213 private static TestCase createFromValueOf( 214 final XPath xpath, final Context context, final Element element) { 215 final String select = element.getAttribute("select"); 216 return new XPathTest(context, select) { 217 @Override void test(Node contextNode) throws XPathExpressionException { 218 assertEquals(element.getTextContent(), 219 xpath.evaluate(select, contextNode, XPathConstants.STRING)); 220 } 221 }; 222 } 223 224 /** 225 * The subject of an XPath query. This is itself defined by an XPath query, 226 * so each test requires at least XPath expressions to be evaluated. 227 */ 228 static class Context { 229 private final InputSource inputSource; 230 private final String url; 231 private final String select; 232 233 Context(InputSource inputSource, String url, String select) { 234 this.inputSource = inputSource; 235 this.url = url; 236 this.select = select; 237 } 238 239 Node getNode() { 240 XPath xpath = XPathFactory.newInstance().newXPath(); 241 try { 242 return (Node) xpath.evaluate(select, inputSource, XPathConstants.NODE); 243 } catch (XPathExpressionException e) { 244 Error error = new AssertionFailedError("Failed to get context"); 245 error.initCause(e); 246 throw error; 247 } 248 } 249 250 @Override public String toString() { 251 return url + " " + select; 252 } 253 } 254 255 /** 256 * This test evaluates an XPath expression against a context node and 257 * compares the result to a known expectation. 258 */ 259 public abstract static class XPathTest extends TestCase { 260 private final Context context; 261 private final String select; 262 263 public XPathTest(Context context, String select) { 264 super("test"); 265 this.context = context; 266 this.select = select; 267 } 268 269 abstract void test(Node contextNode) throws XPathExpressionException; 270 271 public final void test() throws XPathExpressionException { 272 try { 273 test(context.getNode()); 274 } catch (XPathExpressionException e) { 275 if (isMissingFunction(e)) { 276 fail(e.getCause().getMessage()); 277 } else { 278 throw e; 279 } 280 } 281 } 282 283 private boolean isMissingFunction(XPathExpressionException e) { 284 return e.getCause() != null 285 && e.getCause().getMessage().startsWith("Could not find function"); 286 } 287 288 @Override public String getName() { 289 return context + " " + select; 290 } 291 } 292 293 /** 294 * Performs XPath variable resolution by using {@code var:name="value"} 295 * attributes from the given element. 296 */ 297 private static class ElementVariableResolver implements XPathVariableResolver { 298 private final Element element; 299 public ElementVariableResolver(Element element) { 300 this.element = element; 301 } 302 public Object resolveVariable(QName variableName) { 303 return element.getAttribute("var:" + variableName.getLocalPart()); 304 } 305 } 306 307 private static List<Element> elementsOf(NodeList nodeList) { 308 List<Element> result = new ArrayList<Element>(); 309 for (int i = 0; i < nodeList.getLength(); i++) { 310 Node node = nodeList.item(i); 311 if (node instanceof Element) { 312 result.add((Element) node); 313 } 314 } 315 return result; 316 } 317} 318