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 libcore.xml;
18
19import junit.framework.AssertionFailedError;
20import junit.framework.Test;
21import junit.framework.TestCase;
22import junit.framework.TestSuite;
23import org.w3c.dom.Element;
24import org.w3c.dom.Node;
25import org.w3c.dom.NodeList;
26import org.xml.sax.InputSource;
27import org.xml.sax.SAXException;
28
29import javax.xml.namespace.QName;
30import javax.xml.parsers.DocumentBuilderFactory;
31import javax.xml.parsers.ParserConfigurationException;
32import javax.xml.xpath.XPath;
33import javax.xml.xpath.XPathConstants;
34import javax.xml.xpath.XPathExpressionException;
35import javax.xml.xpath.XPathFactory;
36import javax.xml.xpath.XPathVariableResolver;
37import java.io.File;
38import java.io.IOException;
39import java.util.ArrayList;
40import 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 */
54public class JaxenXPathTestSuite {
55
56    private static final String DEFAULT_JAXEN_HOME = "/home/dalvik-prebuild/jaxen";
57
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     */
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     */
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     */
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