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