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